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 @@ +
RPC
RPC
Main Proxy
Main Proxy
Ext API
Ext API
Registry
Registry
Plugin API
Plugin API
Backend
Backend
Headless Plugin Host
Headless Plugin Host
register(handle, DTO)
register(handle, DTO)
provideItems(handle, args)
provideItems(handle, args)
Main API
Main API
Main Impl
Main Impl
Ext Proxy
Ext Proxy
Contribution (application-defined)
Contribution (ap...
Headless Plugin
Headless P...
Ext Impl
register
register
provideItems
provideIte...
Node
Node
Text is not SVG - cannot display
\ 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 @@ +
RPC Relay
(per frontend connection)
RPC Relay...
Backend
Backend
register(handle, DTO)
register(handle, DTO)
RPC
RPC
Frontend
Frontend
Main API
Main API
Main Impl
Main Impl
Ext Proxy
Ext Proxy
Browser
Browser
Main Proxy
Main Proxy
Ext API
Ext API
Registry
Registry
Plugin API
Plugin API
Plugin Host
Plugin Host
Contribution
(e.g. a code action provider)
Contribution...
Plugin / Extension
Plugin / E...
Ext Impl
register
register
provideItems
provideIte...
RPC
RPC
provideItems(handle, args)
provideItems(handle, args)
Text is not SVG - cannot display
\ 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 @@ +
+ +
+ +theia-ext-logo + +

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 @@ +
+ +
+ +theia-ext-logo + +

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
+
+
+
Enter your question
+
+
+
+ { + inProgress ? { + if (lastRequest) { + props.onCancel(lastRequest); + } + setInProgress(false); + }} /> : + submit(editorRef.current?.document.textEditorModel.getValue() || '')} + style={{ cursor: !props.isEnabled ? 'default' : 'pointer', opacity: !props.isEnabled ? 0.5 : 1 }} + /> + } +
+
; +}; 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 ( +
+
+
{this.renderTitle(response)}
+
+ + +
+
+
+
+ this.handleContextMenuEvent(parentNode, e, response.code)}> +
+
+ ); + } + + protected renderTitle(response: CodeChatResponseContent): ReactNode { + const uri = response.location?.uri; + const position = response.location?.position; + if (uri && position) { + return {this.getTitle(response.location?.uri, response.language)}; + } + return this.getTitle(response.location?.uri, response.language); + } + + private getTitle(uri: URI | undefined, language: string | undefined): string { + // If there is a URI, use the file name as the title. Otherwise, use the language as the title. + // If there is no language, use a generic fallback title. + return uri?.path?.toString().split('/').pop() ?? language ?? 'Generated Code'; + } + + /** + * Opens a file and moves the cursor to the specified position. + * + * @param uri - The URI of the file to open. + * @param position - The position to move the cursor to, specified as {line, character}. + */ + async openFileAtPosition(uri: URI, position: Position): Promise { + const editorWidget = await this.editorManager.open(uri) as EditorWidget; + if (editorWidget) { + const editor = editorWidget.editor; + editor.revealPosition(position); + editor.focus(); + editor.cursor = position; + } + } + + protected handleContextMenuEvent(node: TreeNode | undefined, event: IMouseEvent, code: string): void { + this.contextMenuRenderer.render({ + menuPath: ChatViewTreeWidget.CONTEXT_MENU, + anchor: { x: event.posx, y: event.posy }, + args: [node, { code }] + }); + event.preventDefault(); + } +} + +const CopyToClipboardButton = (props: { code: string, clipboardService: ClipboardService }) => { + const { code, clipboardService } = props; + const copyCodeToClipboard = React.useCallback(() => { + clipboardService.writeText(code); + }, [code, clipboardService]); + return
; +}; + +const InsertCodeAtCursorButton = (props: { code: string, editorManager: EditorManager }) => { + const { code, editorManager } = props; + const insertCode = React.useCallback(() => { + const editor = editorManager.currentEditor; + if (editor) { + const currentEditor = editor.editor; + const selection = currentEditor.selection; + + // Insert the text at the current cursor position + // If there is a selection, replace the selection with the text + currentEditor.executeEdits([{ + range: { + start: selection.start, + end: selection.end + }, + newText: code + }]); + } + }, [code, editorManager]); + return
; +}; + +/** + * Renders the given code within a Monaco Editor + */ +export const CodeWrapper = (props: { + content: string, + language?: string, + untitledResourceResolver: UntitledResourceResolver, + editorProvider: MonacoEditorProvider, + contextMenuCallback: (e: IMouseEvent) => void +}) => { + // eslint-disable-next-line no-null/no-null + const ref = React.useRef(null); + const editorRef = React.useRef(undefined); + + const createInputElement = async () => { + const resource = await props.untitledResourceResolver.createUntitledResource(undefined, props.language); + const editor = await props.editorProvider.createInline(resource.uri, ref.current!, { + readOnly: true, + autoSizing: true, + scrollBeyondLastLine: false, + scrollBeyondLastColumn: 0, + renderFinalNewline: 'off', + maxHeight: -1, + scrollbar: { vertical: 'hidden' }, + codeLens: false, + inlayHints: { enabled: 'off' }, + hover: { enabled: false } + }); + editor.document.textEditorModel.setValue(props.content); + editor.getControl().onContextMenu(e => props.contextMenuCallback(e.event)); + editorRef.current = editor; + }; + + React.useEffect(() => { + createInputElement(); + return () => { + if (editorRef.current) { + editorRef.current.dispose(); + } + }; + }, []); + + React.useEffect(() => { + if (editorRef.current) { + editorRef.current.document.textEditorModel.setValue(props.content); + } + }, [props.content]); + + editorRef.current?.resizeToFit(); + + return
; +}; diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/command-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/command-part-renderer.tsx new file mode 100644 index 0000000000000..8494742fbe659 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/command-part-renderer.tsx @@ -0,0 +1,60 @@ +// ***************************************************************************** +// 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 { ChatResponsePartRenderer } from '../chat-response-part-renderer'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ChatResponseContent, CommandChatResponseContent } from '@theia/ai-chat/lib/common'; +import { ReactNode } from '@theia/core/shared/react'; +import * as React from '@theia/core/shared/react'; +import { CommandRegistry, CommandService } from '@theia/core'; + +@injectable() +export class CommandPartRenderer implements ChatResponsePartRenderer { + @inject(CommandService) private commandService: CommandService; + @inject(CommandRegistry) private commandRegistry: CommandRegistry; + canHandle(response: ChatResponseContent): number { + if (CommandChatResponseContent.is(response)) { + return 10; + } + return -1; + } + render(response: CommandChatResponseContent): ReactNode { + const label = + response.customCallback?.label ?? + response.command?.label ?? + response.command?.id + .split('-') + .map(s => s[0].toUpperCase() + s.substring(1)) + .join(' ') ?? 'Execute'; + if (!response.customCallback && response.command) { + const isCommandEnabled = this.commandRegistry.isEnabled(response.command.id); + if (!isCommandEnabled) { + return
The command has the id "{response.command.id}" but it is not executable from the Chat window.
; + + } + } + return ; + } + private onCommand(arg: CommandChatResponseContent): void { + if (arg.customCallback) { + arg.customCallback.callback().catch(e => { console.error(e); }); + } else if (arg.command) { + this.commandService.executeCommand(arg.command.id, ...(arg.arguments ?? [])).catch(e => { console.error(e); }); + } else { + console.warn('No command or custom callback provided in command chat response content'); + } + } +} diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/error-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/error-part-renderer.tsx new file mode 100644 index 0000000000000..4a8fc593d9b50 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/error-part-renderer.tsx @@ -0,0 +1,35 @@ +// ***************************************************************************** +// 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 { ChatResponsePartRenderer } from '../chat-response-part-renderer'; +import { injectable } from '@theia/core/shared/inversify'; +import { ChatResponseContent, ErrorChatResponseContent } from '@theia/ai-chat/lib/common'; +import { ReactNode } from '@theia/core/shared/react'; +import * as React from '@theia/core/shared/react'; + +@injectable() +export class ErrorPartRenderer implements ChatResponsePartRenderer { + canHandle(response: ChatResponseContent): number { + if (ErrorChatResponseContent.is(response)) { + return 10; + } + return -1; + } + render(response: ErrorChatResponseContent): ReactNode { + return
{response.error.message}
; + } + +} diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/horizontal-layout-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/horizontal-layout-part-renderer.tsx new file mode 100644 index 0000000000000..79ef8d1b9e483 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/horizontal-layout-part-renderer.tsx @@ -0,0 +1,59 @@ +// ***************************************************************************** +// 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 { ChatResponsePartRenderer } from '../chat-response-part-renderer'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { + ChatResponseContent, + HorizontalLayoutChatResponseContent, +} from '@theia/ai-chat/lib/common'; +import { ReactNode } from '@theia/core/shared/react'; +import * as React from '@theia/core/shared/react'; +import { ContributionProvider } from '@theia/core'; +import { ResponseNode } from '../chat-tree-view/chat-view-tree-widget'; + +@injectable() +export class HorizontalLayoutPartRenderer + implements ChatResponsePartRenderer { + @inject(ContributionProvider) + @named(ChatResponsePartRenderer) + protected readonly chatResponsePartRenderers: ContributionProvider< + ChatResponsePartRenderer + >; + + canHandle(response: ChatResponseContent): number { + if (HorizontalLayoutChatResponseContent.is(response)) { + return 10; + } + return -1; + } + render(response: HorizontalLayoutChatResponseContent, parentNode: ResponseNode): ReactNode { + const contributions = this.chatResponsePartRenderers.getContributions(); + return ( +
+ {response.content.map(content => { + const renderer = contributions + .map(c => ({ + prio: c.canHandle(content), + renderer: c, + })) + .sort((a, b) => b.prio - a.prio)[0].renderer; + return renderer.render(content, parentNode); + })} +
+ ); + } +} diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/index.ts b/packages/ai-chat-ui/src/browser/chat-response-renderer/index.ts new file mode 100644 index 0000000000000..b0846e52fe1c2 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/index.ts @@ -0,0 +1,23 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +export * from './ai-editor-manager'; +export * from './code-part-renderer'; +export * from './command-part-renderer'; +export * from './error-part-renderer'; +export * from './horizontal-layout-part-renderer'; +export * from './markdown-part-renderer'; +export * from './text-part-renderer'; +export * from './toolcall-part-renderer'; diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx new file mode 100644 index 0000000000000..8fba51ad2fcbd --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx @@ -0,0 +1,90 @@ +// ***************************************************************************** +// 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 { ChatResponsePartRenderer } from '../chat-response-part-renderer'; +import { injectable } from '@theia/core/shared/inversify'; +import { + ChatResponseContent, + InformationalChatResponseContent, + MarkdownChatResponseContent, +} from '@theia/ai-chat/lib/common'; +import { ReactNode, useEffect, useRef } from '@theia/core/shared/react'; +import * as React from '@theia/core/shared/react'; +import * as markdownit from '@theia/core/shared/markdown-it'; +import * as DOMPurify from '@theia/core/shared/dompurify'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; + +@injectable() +export class MarkdownPartRenderer implements ChatResponsePartRenderer { + protected readonly markdownIt = markdownit(); + canHandle(response: ChatResponseContent): number { + if (MarkdownChatResponseContent.is(response)) { + return 10; + } + if (InformationalChatResponseContent.is(response)) { + return 10; + } + return -1; + } + render(response: MarkdownChatResponseContent | InformationalChatResponseContent): ReactNode { + // TODO let the user configure whether they want to see informational content + if (InformationalChatResponseContent.is(response)) { + // null is valid in React + // eslint-disable-next-line no-null/no-null + return null; + } + + return ; + } + +} + +const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent | InformationalChatResponseContent }) => { + const ref = useMarkdownRendering(response.content); + + return
; +}; + +/** + * This hook uses markdown-it directly to render markdown. + * The reason to use markdown-it directly is that the MarkdownRenderer is + * overriden by theia with a monaco version. This monaco version strips all html + * tags from the markdown with empty content. + * This leads to unexpected behavior when rendering markdown with html tags. + * + * @param markdown the string to render as markdown + * @returns the ref to use in an element to render the markdown + */ +export const useMarkdownRendering = (markdown: string | MarkdownString) => { + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + const markdownString = typeof markdown === 'string' ? markdown : markdown.value; + useEffect(() => { + const markdownIt = markdownit(); + const host = document.createElement('div'); + const html = markdownIt.render(markdownString); + host.innerHTML = DOMPurify.sanitize(html, { + ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs + }); + while (ref?.current?.firstChild) { + ref.current.removeChild(ref.current.firstChild); + } + + ref?.current?.appendChild(host); + }, [markdownString]); + + return ref; +}; diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/text-part-renderer.spec.ts b/packages/ai-chat-ui/src/browser/chat-response-renderer/text-part-renderer.spec.ts new file mode 100644 index 0000000000000..e67b0fe0b122a --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/text-part-renderer.spec.ts @@ -0,0 +1,50 @@ +// ***************************************************************************** +// 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 { TextPartRenderer } from './text-part-renderer'; +import { expect } from 'chai'; +import { ChatResponseContent } from '@theia/ai-chat'; + +describe('TextPartRenderer', () => { + + it('accepts all parts', () => { + const renderer = new TextPartRenderer(); + expect(renderer.canHandle({ kind: 'text' })).to.be.greaterThan(0); + expect(renderer.canHandle({ kind: 'code' })).to.be.greaterThan(0); + expect(renderer.canHandle({ kind: 'command' })).to.be.greaterThan(0); + expect(renderer.canHandle({ kind: 'error' })).to.be.greaterThan(0); + expect(renderer.canHandle({ kind: 'horizontal' })).to.be.greaterThan(0); + expect(renderer.canHandle({ kind: 'informational' })).to.be.greaterThan(0); + expect(renderer.canHandle({ kind: 'markdownContent' })).to.be.greaterThan(0); + expect(renderer.canHandle({ kind: 'toolCall' })).to.be.greaterThan(0); + expect(renderer.canHandle(undefined as unknown as ChatResponseContent)).to.be.greaterThan(0); + }); + + it('renders text correctly', () => { + const renderer = new TextPartRenderer(); + const part = { kind: 'text', asString: () => 'Hello, World!' }; + const node = renderer.render(part); + expect(JSON.stringify(node)).to.contain('Hello, World!'); + }); + + it('handles undefined content gracefully', () => { + const renderer = new TextPartRenderer(); + const part = undefined as unknown as ChatResponseContent; + const node = renderer.render(part); + expect(node).to.exist; + }); + +}); diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/text-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/text-part-renderer.tsx new file mode 100644 index 0000000000000..61ba2ed813e41 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/text-part-renderer.tsx @@ -0,0 +1,35 @@ +// ***************************************************************************** +// 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 { ChatResponsePartRenderer } from '../chat-response-part-renderer'; +import { injectable } from '@theia/core/shared/inversify'; +import { ChatResponseContent } from '@theia/ai-chat/lib/common'; +import { ReactNode } from '@theia/core/shared/react'; +import * as React from '@theia/core/shared/react'; + +@injectable() +export class TextPartRenderer implements ChatResponsePartRenderer { + canHandle(_reponse: ChatResponseContent): number { + // this is the fallback renderer + return 1; + } + render(response: ChatResponseContent): ReactNode { + if (response && ChatResponseContent.hasAsString(response)) { + return {response.asString()}; + } + return Can't display response, please check your ChatResponsePartRenderers! {JSON.stringify(response)}; + } +} diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/toolcall-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/toolcall-part-renderer.tsx new file mode 100644 index 0000000000000..4f5bc127f285b --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/toolcall-part-renderer.tsx @@ -0,0 +1,60 @@ +// ***************************************************************************** +// 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 { ChatResponsePartRenderer } from '../chat-response-part-renderer'; +import { injectable } from '@theia/core/shared/inversify'; +import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common'; +import { ReactNode } from '@theia/core/shared/react'; +import * as React from '@theia/core/shared/react'; + +@injectable() +export class ToolCallPartRenderer implements ChatResponsePartRenderer { + + canHandle(response: ChatResponseContent): number { + if (ToolCallChatResponseContent.is(response)) { + return 10; + } + return -1; + } + render(response: ToolCallChatResponseContent): ReactNode { + return

+ {response.finished ? +
+ Ran {response.name} +
{this.tryPrettyPrintJson(response)}
+
+ : Running [{response.name}] + } +

; + + } + + private tryPrettyPrintJson(response: ToolCallChatResponseContent): string | undefined { + let responseContent = response.result; + try { + if (response.result) { + responseContent = JSON.stringify(JSON.parse(response.result), undefined, 2); + } + } catch (e) { + // fall through + } + return responseContent; + } +} + +const Spinner = () => ( + +); diff --git a/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-container.ts b/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-container.ts new file mode 100644 index 0000000000000..9550de109ec0a --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-container.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// 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 { createTreeContainer, TreeProps } from '@theia/core/lib/browser'; +import { interfaces } from '@theia/core/shared/inversify'; +import { ChatViewTreeWidget } from './chat-view-tree-widget'; + +const CHAT_VIEW_TREE_PROPS = { + multiSelect: false, + search: false, +} as TreeProps; + +export function createChatViewTreeWidget(parent: interfaces.Container): ChatViewTreeWidget { + const child = createTreeContainer(parent, { + props: CHAT_VIEW_TREE_PROPS, + widget: ChatViewTreeWidget, + }); + return child.get(ChatViewTreeWidget); +} diff --git a/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx b/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx new file mode 100644 index 0000000000000..19d7daacdd9b8 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx @@ -0,0 +1,403 @@ +// ***************************************************************************** +// 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 { + ChatAgentService, + ChatModel, + ChatProgressMessage, + ChatRequestModel, + ChatResponseContent, + ChatResponseModel, +} from '@theia/ai-chat'; +import { CommandRegistry, ContributionProvider } from '@theia/core'; +import { + codicon, + CommonCommands, + CompositeTreeNode, + ContextMenuRenderer, + Key, + KeyCode, + NodeProps, + TreeModel, + TreeNode, + TreeProps, + TreeWidget, +} from '@theia/core/lib/browser'; +import { + inject, + injectable, + named, + postConstruct +} from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; + +import { ChatNodeToolbarActionContribution } from '../chat-node-toolbar-action-contribution'; +import { ChatResponsePartRenderer } from '../chat-response-part-renderer'; +import { useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer'; + +// TODO Instead of directly operating on the ChatRequestModel we could use an intermediate view model +export interface RequestNode extends TreeNode { + request: ChatRequestModel +} +export const isRequestNode = (node: TreeNode): node is RequestNode => 'request' in node; + +// TODO Instead of directly operating on the ChatResponseModel we could use an intermediate view model +export interface ResponseNode extends TreeNode { + response: ChatResponseModel +} +export const isResponseNode = (node: TreeNode): node is ResponseNode => 'response' in node; + +function isEnterKey(e: React.KeyboardEvent): boolean { + return Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode; +} + +@injectable() +export class ChatViewTreeWidget extends TreeWidget { + static readonly ID = 'chat-tree-widget'; + static readonly CONTEXT_MENU = ['chat-tree-context-menu']; + + @inject(ContributionProvider) @named(ChatResponsePartRenderer) + protected readonly chatResponsePartRenderers: ContributionProvider>; + + @inject(ContributionProvider) @named(ChatNodeToolbarActionContribution) + protected readonly chatNodeToolbarActionContributions: ContributionProvider; + + @inject(ChatAgentService) + protected chatAgentService: ChatAgentService; + + @inject(CommandRegistry) + private commandRegistry: CommandRegistry; + + protected _shouldScrollToEnd = true; + + protected isEnabled = false; + + set shouldScrollToEnd(shouldScrollToEnd: boolean) { + this._shouldScrollToEnd = shouldScrollToEnd; + this.shouldScrollToRow = this._shouldScrollToEnd; + } + + get shouldScrollToEnd(): boolean { + return this._shouldScrollToEnd; + } + + constructor( + @inject(TreeProps) props: TreeProps, + @inject(TreeModel) model: TreeModel, + @inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer + ) { + super(props, model, contextMenuRenderer); + + this.id = ChatViewTreeWidget.ID; + this.title.closable = false; + + model.root = { + id: 'ChatTree', + name: 'ChatRootNode', + parent: undefined, + visible: false, + children: [], + } as CompositeTreeNode; + } + + @postConstruct() + protected override init(): void { + super.init(); + + this.id = ChatViewTreeWidget.ID + '-treeContainer'; + this.addClass('treeContainer'); + } + + public setEnabled(enabled: boolean): void { + this.isEnabled = enabled; + this.update(); + } + + protected override renderTree(model: TreeModel): React.ReactNode { + if (this.isEnabled) { + return super.renderTree(model); + } + return this.renderDisabledMessage(); + } + + private renderDisabledMessage(): React.ReactNode { + return
+
+
+ 🚀 Experimental AI Feature Available! +
+

Currently, all AI Features are disabled!

+
+
+

How to Enable Experimental AI Features:

+
+
+

To enable the experimental AI features, please go to   + {this.renderLinkButton('the settings menu', CommonCommands.OPEN_PREFERENCES.id)} +  and locate the AI Features section.

+
    +
  1. Toggle the switch for 'Ai-features: Enable'.
  2. +
  3. Provide at least one LLM provider (e.g. OpenAI), also see the documentation + for more information.
  4. +
+

This will activate the new AI capabilities in the app. Please remember, these features are still in development, so they may change or be unstable. 🚧

+
+ +
+

Currently Supported Views and Features:

+
+
+

Once the experimental AI features are enabled, you can access the following views and features:

+
    +
  • Code Completion
  • +
  • Terminal Assistance (via CTRL+I in a terminal)
  • +
  • This Chat View (features the following agents): +
      +
    • Universal Chat Agent
    • +
    • Workspace Chat Agent
    • +
    • Command Chat Agent
    • +
    • Orchestrator Chat Agent
    • +
    +
  • +
  • {this.renderLinkButton('AI History View', 'aiHistory:open')}
  • +
  • {this.renderLinkButton('AI Configuration View', 'aiConfiguration:open')}
  • +
+

See the documentation for more information.

+
+
+
+
; + } + + private renderLinkButton(title: string, openCommandId: string): React.ReactNode { + return this.commandRegistry.executeCommand(openCommandId)} + onKeyDown={e => isEnterKey(e) && this.commandRegistry.executeCommand(openCommandId)}> + {title} + ; + } + + private mapRequestToNode(request: ChatRequestModel): RequestNode { + return { + id: request.id, + parent: this.model.root as CompositeTreeNode, + request + }; + } + + private mapResponseToNode(response: ChatResponseModel): ResponseNode { + return { + id: response.id, + parent: this.model.root as CompositeTreeNode, + response + }; + } + + /** + * Tracks the ChatModel handed over. + * Tracking multiple chat models will result in a weird UI + */ + public trackChatModel(chatModel: ChatModel): void { + this.recreateModelTree(chatModel); + chatModel.getRequests().forEach(request => { + if (!request.response.isComplete) { + request.response.onDidChange(() => this.scheduleUpdateScrollToRow()); + } + }); + this.toDispose.push( + chatModel.onDidChange(event => { + this.recreateModelTree(chatModel); + if (event.kind === 'addRequest' && !event.request.response.isComplete) { + event.request.response.onDidChange(() => this.scheduleUpdateScrollToRow()); + } + }) + ); + } + + protected override getScrollToRow(): number | undefined { + if (this.shouldScrollToEnd) { + return this.rows.size; + } + return super.getScrollToRow(); + } + + private async recreateModelTree(chatModel: ChatModel): Promise { + if (CompositeTreeNode.is(this.model.root)) { + const nodes: TreeNode[] = []; + chatModel.getRequests().forEach(request => { + nodes.push(this.mapRequestToNode(request)); + nodes.push(this.mapResponseToNode(request.response)); + }); + this.model.root.children = nodes; + this.model.refresh(); + } + } + + protected override renderNode( + node: TreeNode, + props: NodeProps + ): React.ReactNode { + if (!TreeNode.isVisible(node)) { + return undefined; + } + if (!(isRequestNode(node) || isResponseNode(node))) { + return super.renderNode(node, props); + } + return +
this.handleContextMenu(node, e)}> + {this.renderAgent(node)} + {this.renderDetail(node)} +
+
; + } + + private renderAgent(node: RequestNode | ResponseNode): React.ReactNode { + const inProgress = isResponseNode(node) && !node.response.isComplete && !node.response.isCanceled && !node.response.isError; + const toolbarContributions = !inProgress + ? this.chatNodeToolbarActionContributions.getContributions() + .flatMap(c => c.getToolbarActions(node)) + .filter(action => this.commandRegistry.isEnabled(action.commandId, node)) + .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)) + : []; + return +
+
+

{this.getAgentLabel(node)}

+ {inProgress && Generating} +
+ {!inProgress && + toolbarContributions.length > 0 && + toolbarContributions.map(action => + { + e.stopPropagation(); + this.commandRegistry.executeCommand(action.commandId, node); + }} + onKeyDown={e => { + if (isEnterKey(e)) { + e.stopPropagation(); + this.commandRegistry.executeCommand(action.commandId, node); + } + }} + role='button' + > + )} +
+
+
; + } + + private getAgentLabel(node: RequestNode | ResponseNode): string { + if (isRequestNode(node)) { + // TODO find user name + return 'You'; + } + const agent = node.response.agentId ? this.chatAgentService.getAgent(node.response.agentId) : undefined; + return agent?.name ?? 'AI'; + } + + private getAgentIconClassName(node: RequestNode | ResponseNode): string | undefined { + if (isRequestNode(node)) { + return codicon('account'); + } + + const agent = node.response.agentId ? this.chatAgentService.getAgent(node.response.agentId) : undefined; + return agent?.iconClass ?? codicon('copilot'); + } + + private renderDetail(node: RequestNode | ResponseNode): React.ReactNode { + if (isRequestNode(node)) { + return this.renderChatRequest(node); + } + if (isResponseNode(node)) { + return this.renderChatResponse(node); + }; + } + + private renderChatRequest(node: RequestNode): React.ReactNode { + return ; + } + + private renderChatResponse(node: ResponseNode): React.ReactNode { + return ( +
+ {!node.response.isComplete + && node.response.response.content.length === 0 + && node.response.progressMessages.map((c, i) => + + )} + {node.response.response.content.map((c, i) => +
{this.getChatResponsePartRenderer(c, node)}
+ )} +
+ ); + } + + private getChatResponsePartRenderer(content: ChatResponseContent, node: ResponseNode): React.ReactNode { + const renderer = this.chatResponsePartRenderers.getContributions().reduce<[number, ChatResponsePartRenderer | undefined]>( + (prev, current) => { + const prio = current.canHandle(content); + if (prio > prev[0]) { + return [prio, current]; + } return prev; + }, + [-1, undefined])[1]; + if (!renderer) { + console.error('No renderer found for content', content); + return
Error: No renderer found
; + } + return renderer.render(content, node); + } + + protected handleContextMenu(node: TreeNode | undefined, event: React.MouseEvent): void { + this.contextMenuRenderer.render({ + menuPath: ChatViewTreeWidget.CONTEXT_MENU, + anchor: { x: event.clientX, y: event.clientY }, + args: [node] + }); + event.preventDefault(); + } +} + +const ChatRequestRender = ({ node }: { node: RequestNode }) => { + const text = node.request.request.displayText ?? node.request.request.text; + const ref = useMarkdownRendering(text); + + return
; +}; + +const ProgressMessage = (c: ChatProgressMessage) => ( +
+ {c.content} +
+); + +const Indicator = (progressMessage: ChatProgressMessage) => ( + + {progressMessage.status === 'inProgress' && + + } + {progressMessage.status === 'completed' && + + } + {progressMessage.status === 'failed' && + + } + +); diff --git a/packages/filesystem/src/typings/nsfw/index.d.ts b/packages/ai-chat-ui/src/browser/chat-tree-view/index.ts similarity index 84% rename from packages/filesystem/src/typings/nsfw/index.d.ts rename to packages/ai-chat-ui/src/browser/chat-tree-view/index.ts index 0573effb0d4c1..b3a2fd606e01a 100644 --- a/packages/filesystem/src/typings/nsfw/index.d.ts +++ b/packages/ai-chat-ui/src/browser/chat-tree-view/index.ts @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright (C) 2018 Ericsson and others. +// 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 @@ -14,5 +14,5 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -// eslint-disable-next-line spaced-comment -/// +export * from './chat-view-tree-container'; +export * from './chat-view-tree-widget'; diff --git a/packages/ai-chat-ui/src/browser/chat-view-commands.ts b/packages/ai-chat-ui/src/browser/chat-view-commands.ts new file mode 100644 index 0000000000000..f513e32690444 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-view-commands.ts @@ -0,0 +1,45 @@ +// ***************************************************************************** +// 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 { Command, nls } from '@theia/core'; +import { codicon } from '@theia/core/lib/browser'; + +export namespace ChatCommands { + const CHAT_CATEGORY = 'Chat'; + const CHAT_CATEGORY_KEY = nls.getDefaultKey(CHAT_CATEGORY); + + export const SCROLL_LOCK_WIDGET = Command.toLocalizedCommand({ + id: 'chat:widget:lock', + category: CHAT_CATEGORY, + iconClass: codicon('unlock') + }, '', CHAT_CATEGORY_KEY); + + export const SCROLL_UNLOCK_WIDGET = Command.toLocalizedCommand({ + id: 'chat:widget:unlock', + category: CHAT_CATEGORY, + iconClass: codicon('lock') + }, '', CHAT_CATEGORY_KEY); +} + +export const AI_CHAT_NEW_CHAT_WINDOW_COMMAND: Command = { + id: 'ai-chat-ui.new-chat', + iconClass: codicon('add') +}; + +export const AI_CHAT_SHOW_CHATS_COMMAND: Command = { + id: 'ai-chat-ui.show-chats', + iconClass: codicon('history') +}; diff --git a/packages/ai-chat-ui/src/browser/chat-view-contribution.ts b/packages/ai-chat-ui/src/browser/chat-view-contribution.ts new file mode 100644 index 0000000000000..a94c2d2eeeadd --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-view-contribution.ts @@ -0,0 +1,154 @@ +// ***************************************************************************** +// 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 { Command, CommandContribution, CommandRegistry, CommandService, isObject, MenuContribution, MenuModelRegistry } from '@theia/core'; +import { CommonCommands, TreeNode } from '@theia/core/lib/browser'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ChatViewTreeWidget, isRequestNode, isResponseNode, RequestNode, ResponseNode } from './chat-tree-view/chat-view-tree-widget'; +import { AIChatInputWidget } from './chat-input-widget'; + +export namespace ChatViewCommands { + export const COPY_MESSAGE = Command.toDefaultLocalizedCommand({ + id: 'chat.copy.message', + label: 'Copy Message' + }); + export const COPY_ALL = Command.toDefaultLocalizedCommand({ + id: 'chat.copy.all', + label: 'Copy All' + }); + export const COPY_CODE = Command.toDefaultLocalizedCommand({ + id: 'chat.copy.code', + label: 'Copy Code Block' + }); +} + +@injectable() +export class ChatViewMenuContribution implements MenuContribution, CommandContribution { + + @inject(ClipboardService) + protected readonly clipboardService: ClipboardService; + + @inject(CommandService) + protected readonly commandService: CommandService; + + registerCommands(commands: CommandRegistry): void { + commands.registerHandler(CommonCommands.COPY.id, { + execute: (...args: unknown[]) => { + if (window.getSelection()?.type !== 'Range' && containsRequestOrResponseNode(args)) { + this.copyMessage(extractRequestOrResponseNodes(args)); + } else { + this.commandService.executeCommand(CommonCommands.COPY.id); + } + }, + isEnabled: (...args: unknown[]) => containsRequestOrResponseNode(args) + }); + commands.registerCommand(ChatViewCommands.COPY_MESSAGE, { + execute: (...args: unknown[]) => { + if (containsRequestOrResponseNode(args)) { + this.copyMessage(extractRequestOrResponseNodes(args)); + } + }, + isEnabled: (...args: unknown[]) => containsRequestOrResponseNode(args) + }); + commands.registerCommand(ChatViewCommands.COPY_ALL, { + execute: (...args: unknown[]) => { + if (containsRequestOrResponseNode(args)) { + const parent = extractRequestOrResponseNodes(args).find(arg => arg.parent)?.parent; + const text = parent?.children + .filter(isRequestOrResponseNode) + .map(child => this.getText(child)) + .join('\n\n---\n\n'); + if (text) { + this.clipboardService.writeText(text); + } + } + }, + isEnabled: (...args: unknown[]) => containsRequestOrResponseNode(args) + }); + commands.registerCommand(ChatViewCommands.COPY_CODE, { + execute: (...args: unknown[]) => { + if (containsCode(args)) { + const code = args + .filter(isCodeArg) + .map(arg => arg.code) + .join(); + this.clipboardService.writeText(code); + } + }, + isEnabled: (...args: unknown[]) => containsRequestOrResponseNode(args) && containsCode(args) + }); + } + + protected copyMessage(args: (RequestNode | ResponseNode)[]): void { + const text = this.getTextAndJoin(args); + this.clipboardService.writeText(text); + } + + protected getTextAndJoin(args: (RequestNode | ResponseNode)[] | undefined): string { + return args !== undefined ? args.map(arg => this.getText(arg)).join() : ''; + } + + protected getText(arg: RequestNode | ResponseNode): string { + if (isRequestNode(arg)) { + return arg.request.request.text; + } else if (isResponseNode(arg)) { + return arg.response.response.asString(); + } + return ''; + } + + registerMenus(menus: MenuModelRegistry): void { + menus.registerMenuAction([...ChatViewTreeWidget.CONTEXT_MENU, '_1'], { + commandId: CommonCommands.COPY.id + }); + menus.registerMenuAction([...ChatViewTreeWidget.CONTEXT_MENU, '_1'], { + commandId: ChatViewCommands.COPY_MESSAGE.id + }); + menus.registerMenuAction([...ChatViewTreeWidget.CONTEXT_MENU, '_1'], { + commandId: ChatViewCommands.COPY_ALL.id + }); + menus.registerMenuAction([...ChatViewTreeWidget.CONTEXT_MENU, '_1'], { + commandId: ChatViewCommands.COPY_CODE.id + }); + menus.registerMenuAction([...AIChatInputWidget.CONTEXT_MENU, '_1'], { + commandId: CommonCommands.COPY.id + }); + menus.registerMenuAction([...AIChatInputWidget.CONTEXT_MENU, '_1'], { + commandId: CommonCommands.PASTE.id + }); + } + +} + +function extractRequestOrResponseNodes(args: unknown[]): (RequestNode | ResponseNode)[] { + return args.filter(arg => isRequestOrResponseNode(arg)) as (RequestNode | ResponseNode)[]; +} + +function containsRequestOrResponseNode(args: unknown[]): args is (unknown | RequestNode | ResponseNode)[] { + return extractRequestOrResponseNodes(args).length > 0; +} + +function isRequestOrResponseNode(arg: unknown): arg is RequestNode | ResponseNode { + return TreeNode.is(arg) && (isRequestNode(arg) || isResponseNode(arg)); +} + +function containsCode(args: unknown[]): args is (unknown | { code: string })[] { + return args.filter(arg => isCodeArg(arg)).length > 0; +} + +function isCodeArg(arg: unknown): arg is { code: string } { + return isObject(arg) && 'code' in arg; +} diff --git a/packages/ai-chat-ui/src/browser/chat-view-language-contribution.ts b/packages/ai-chat-ui/src/browser/chat-view-language-contribution.ts new file mode 100644 index 0000000000000..1a9f0dcc8138d --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-view-language-contribution.ts @@ -0,0 +1,141 @@ +// ***************************************************************************** +// 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, named } from '@theia/core/shared/inversify'; +import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser'; +import * as monaco from '@theia/monaco-editor-core'; +import { ContributionProvider, MaybePromise } from '@theia/core'; +import { ProviderResult } from '@theia/monaco-editor-core/esm/vs/editor/common/languages'; +import { ChatAgentService } from '@theia/ai-chat'; +import { AIVariableService } from '@theia/ai-core/lib/common'; +import { ToolProvider } from '@theia/ai-core/lib/common/tool-invocation-registry'; + +export const CHAT_VIEW_LANGUAGE_ID = 'theia-ai-chat-view-language'; +export const CHAT_VIEW_LANGUAGE_EXTENSION = 'aichatviewlanguage'; + +@injectable() +export class ChatViewLanguageContribution implements FrontendApplicationContribution { + + @inject(ChatAgentService) + protected readonly agentService: ChatAgentService; + + @inject(AIVariableService) + protected readonly variableService: AIVariableService; + + @inject(ContributionProvider) + @named(ToolProvider) + private providers: ContributionProvider; + + onStart(_app: FrontendApplication): MaybePromise { + console.log('ChatViewLanguageContribution started'); + monaco.languages.register({ id: CHAT_VIEW_LANGUAGE_ID, extensions: [CHAT_VIEW_LANGUAGE_EXTENSION] }); + + monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, { + triggerCharacters: ['@'], + provideCompletionItems: (model, position, _context, _token): ProviderResult => this.provideAgentCompletions(model, position), + }); + monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, { + triggerCharacters: ['#'], + provideCompletionItems: (model, position, _context, _token): ProviderResult => this.provideVariableCompletions(model, position), + }); + monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, { + triggerCharacters: ['~'], + provideCompletionItems: (model, position, _context, _token): ProviderResult => this.provideToolCompletions(model, position), + }); + } + + getCompletionRange(model: monaco.editor.ITextModel, position: monaco.Position, triggerCharacter: string): monaco.Range | undefined { + // Check if the character before the current position is the trigger character + const lineContent = model.getLineContent(position.lineNumber); + const characterBefore = lineContent[position.column - 2]; // Get the character before the current position + + if (characterBefore !== triggerCharacter) { + // Do not return agent suggestions if the user didn't just type the trigger character + return undefined; + } + + // Calculate the range from the position of the '@' character + const wordInfo = model.getWordUntilPosition(position); + return new monaco.Range( + position.lineNumber, + wordInfo.startColumn, + position.lineNumber, + position.column + ); + } + + private getSuggestions( + model: monaco.editor.ITextModel, + position: monaco.Position, + triggerChar: string, + items: T[], + kind: monaco.languages.CompletionItemKind, + getId: (item: T) => string, + getName: (item: T) => string, + getDescription: (item: T) => string + ): ProviderResult { + const completionRange = this.getCompletionRange(model, position, triggerChar); + if (completionRange === undefined) { + return { suggestions: [] }; + } + const suggestions = items.map(item => ({ + insertText: getId(item), + kind: kind, + label: getName(item), + range: completionRange, + detail: getDescription(item), + })); + return { suggestions }; + } + + provideAgentCompletions(model: monaco.editor.ITextModel, position: monaco.Position): ProviderResult { + return this.getSuggestions( + model, + position, + '@', + this.agentService.getAgents(), + monaco.languages.CompletionItemKind.Value, + agent => agent.id, + agent => agent.name, + agent => agent.description + ); + } + + provideVariableCompletions(model: monaco.editor.ITextModel, position: monaco.Position): ProviderResult { + return this.getSuggestions( + model, + position, + '#', + this.variableService.getVariables(), + monaco.languages.CompletionItemKind.Variable, + variable => variable.name, + variable => variable.name, + variable => variable.description + ); + } + + provideToolCompletions(model: monaco.editor.ITextModel, position: monaco.Position): ProviderResult { + return this.getSuggestions( + model, + position, + '~', + this.providers.getContributions().map(provider => provider.getTool()), + monaco.languages.CompletionItemKind.Function, + tool => tool.id, + tool => tool.name, + tool => tool.description ?? '' + ); + } +} diff --git a/packages/ai-chat-ui/src/browser/chat-view-widget-toolbar-contribution.tsx b/packages/ai-chat-ui/src/browser/chat-view-widget-toolbar-contribution.tsx new file mode 100644 index 0000000000000..85846de3bed08 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-view-widget-toolbar-contribution.tsx @@ -0,0 +1,54 @@ +// ***************************************************************************** +// 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, postConstruct } from '@theia/core/shared/inversify'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { AIChatContribution } from './ai-chat-ui-contribution'; +import { Emitter, nls } from '@theia/core'; +import { ChatCommands } from './chat-view-commands'; + +@injectable() +export class ChatViewWidgetToolbarContribution implements TabBarToolbarContribution { + @inject(AIChatContribution) + protected readonly chatContribution: AIChatContribution; + + protected readonly onChatWidgetStateChangedEmitter = new Emitter(); + protected readonly onChatWidgetStateChanged = this.onChatWidgetStateChangedEmitter.event; + + @postConstruct() + protected init(): void { + this.chatContribution.widget.then(widget => { + widget.onStateChanged(() => this.onChatWidgetStateChangedEmitter.fire()); + }); + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: ChatCommands.SCROLL_LOCK_WIDGET.id, + command: ChatCommands.SCROLL_LOCK_WIDGET.id, + tooltip: nls.localizeByDefault('Turn Auto Scrolling Off'), + onDidChange: this.onChatWidgetStateChanged, + priority: 2 + }); + registry.registerItem({ + id: ChatCommands.SCROLL_UNLOCK_WIDGET.id, + command: ChatCommands.SCROLL_UNLOCK_WIDGET.id, + tooltip: nls.localizeByDefault('Turn Auto Scrolling On'), + onDidChange: this.onChatWidgetStateChanged, + priority: 2 + }); + } +} diff --git a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx new file mode 100644 index 0000000000000..7763b2a8ec8c3 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx @@ -0,0 +1,194 @@ +// ***************************************************************************** +// 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 { CommandService, deepClone, Emitter, Event, MessageService } from '@theia/core'; +import { ChatRequest, ChatRequestModel, ChatRequestModelImpl, ChatService, ChatSession } from '@theia/ai-chat'; +import { BaseWidget, codicon, ExtractableWidget, PanelLayout, PreferenceService, StatefulWidget } from '@theia/core/lib/browser'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { AIChatInputWidget } from './chat-input-widget'; +import { ChatViewTreeWidget } from './chat-tree-view/chat-view-tree-widget'; +import { AIActivationService } from '@theia/ai-core/lib/browser/ai-activation-service'; + +export namespace ChatViewWidget { + export interface State { + locked?: boolean; + } +} + +@injectable() +export class ChatViewWidget extends BaseWidget implements ExtractableWidget, StatefulWidget { + + public static ID = 'chat-view-widget'; + static LABEL = `✨ ${nls.localizeByDefault('Chat')} [Experimental]`; + + @inject(ChatService) + protected chatService: ChatService; + + @inject(MessageService) + protected messageService: MessageService; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + @inject(CommandService) + protected readonly commandService: CommandService; + + @inject(AIActivationService) + protected readonly activationService: AIActivationService; + + protected chatSession: ChatSession; + + protected _state: ChatViewWidget.State = { locked: false }; + protected readonly onStateChangedEmitter = new Emitter(); + + secondaryWindow: Window | undefined; + + constructor( + @inject(ChatViewTreeWidget) + readonly treeWidget: ChatViewTreeWidget, + @inject(AIChatInputWidget) + readonly inputWidget: AIChatInputWidget + ) { + super(); + this.id = ChatViewWidget.ID; + this.title.label = ChatViewWidget.LABEL; + this.title.caption = ChatViewWidget.LABEL; + this.title.iconClass = codicon('comment-discussion'); + this.title.closable = true; + this.node.classList.add('chat-view-widget'); + this.update(); + } + + @postConstruct() + protected init(): void { + this.toDispose.pushAll([ + this.treeWidget, + this.inputWidget, + this.onStateChanged(newState => { + this.treeWidget.shouldScrollToEnd = !newState.locked; + this.update(); + }) + ]); + const layout = this.layout = new PanelLayout(); + + this.treeWidget.node.classList.add('chat-tree-view-widget'); + layout.addWidget(this.treeWidget); + this.inputWidget.node.classList.add('chat-input-widget'); + layout.addWidget(this.inputWidget); + this.chatSession = this.chatService.createSession(); + + this.inputWidget.onQuery = this.onQuery.bind(this); + this.inputWidget.onCancel = this.onCancel.bind(this); + this.inputWidget.chatModel = this.chatSession.model; + this.treeWidget.trackChatModel(this.chatSession.model); + + this.initListeners(); + + this.inputWidget.setEnabled(this.activationService.isActive); + this.treeWidget.setEnabled(this.activationService.isActive); + + this.activationService.onDidChangeActiveStatus(change => { + this.treeWidget.setEnabled(change); + this.inputWidget.setEnabled(change); + this.update(); + }); + } + + protected initListeners(): void { + this.toDispose.push( + this.chatService.onActiveSessionChanged(event => { + const session = event.sessionId ? this.chatService.getSession(event.sessionId) : this.chatService.createSession(); + if (session) { + this.chatSession = session; + this.treeWidget.trackChatModel(this.chatSession.model); + this.inputWidget.chatModel = this.chatSession.model; + if (event.focus) { + this.show(); + } + } else { + console.warn(`Session with ${event.sessionId} not found.`); + } + }) + ); + } + + storeState(): object { + return this.state; + } + + restoreState(oldState: object & Partial): void { + const copy = deepClone(this.state); + if (oldState.locked) { + copy.locked = oldState.locked; + } + this.state = copy; + } + + protected get state(): ChatViewWidget.State { + return this._state; + } + + protected set state(state: ChatViewWidget.State) { + this._state = state; + this.onStateChangedEmitter.fire(this._state); + } + + get onStateChanged(): Event { + return this.onStateChangedEmitter.event; + } + + protected async onQuery(query: string): Promise { + if (query.length === 0) { return; } + + const chatRequest: ChatRequest = { + text: query + }; + + const requestProgress = await this.chatService.sendRequest(this.chatSession.id, chatRequest); + requestProgress?.responseCompleted.then(responseModel => { + if (responseModel.isError) { + this.messageService.error(responseModel.errorObject?.message ?? 'An error occurred druring chat service invocation.'); + } + }); + if (!requestProgress) { + this.messageService.error(`Was not able to send request "${chatRequest.text}" to session ${this.chatSession.id}`); + return; + } + // Tree Widget currently tracks the ChatModel itself. Therefore no notification necessary. + } + + protected onCancel(requestModel: ChatRequestModel): void { + // TODO we should pass a cancellation token with the request (or retrieve one from the request invocation) so we can cleanly cancel here + // For now we cancel manually via casting + (requestModel as ChatRequestModelImpl).response.cancel(); + } + + lock(): void { + this.state = { ...deepClone(this.state), locked: true }; + } + + unlock(): void { + this.state = { ...deepClone(this.state), locked: false }; + } + + get isLocked(): boolean { + return !!this.state.locked; + } + + get isExtractable(): boolean { + return this.secondaryWindow === undefined; + } +} diff --git a/packages/ai-chat-ui/src/browser/style/index.css b/packages/ai-chat-ui/src/browser/style/index.css new file mode 100644 index 0000000000000..904893f87cf24 --- /dev/null +++ b/packages/ai-chat-ui/src/browser/style/index.css @@ -0,0 +1,357 @@ +.chat-view-widget { + display: flex; + flex-direction: column; +} + +.chat-tree-view-widget { + flex: 1; +} + +.chat-input-widget > .ps__rail-x, +.chat-input-widget > .ps__rail-y { + display: none !important; +} + +.theia-ChatNode { + cursor: default; + display: flex; + flex-direction: column; + padding: 16px 20px 6px 20px; + user-select: text; + -webkit-user-select: text; + border-bottom: 1px solid var(--theia-sideBarSectionHeader-border); + overflow-wrap: break-word; +} + +div:last-child > .theia-ChatNode { + border: none; +} + +.theia-ChatNodeHeader { + align-items: center; + display: flex; + justify-content: space-between; + height: 24px; + gap: 8px; + width: 100%; +} + +.theia-ChatNodeHeader .theia-AgentAvatar { + pointer-events: none; + user-select: none; + font-size: 20px; +} + +.theia-ChatNodeHeader .theia-AgentLabel { + font-size: 13px; + font-weight: 600; + margin: 0; +} + +.theia-ChatNodeHeader .theia-ChatContentInProgress { + color: var(--theia-disabledForeground); +} + +.theia-ChatNodeHeader .theia-ChatContentInProgress-Cancel { + position: absolute; + z-index: 999; + right: 20px; +} + +@keyframes dots { + 0%, + 20% { + content: ""; + } + + 40% { + content: "."; + } + + 60% { + content: ".."; + } + + 80%, + 100% { + content: "..."; + } +} + +.theia-ChatNodeHeader .theia-ChatContentInProgress::after { + content: ""; + animation: dots 1s steps(1, end) infinite; +} + +.theia-ChatNode .codicon { + text-align: left; +} + +.theia-AgentLabel { + font-weight: 600; +} + +.theia-ChatNode .theia-ChatNodeToolbar { + margin-left: auto; + line-height: 18px; +} +.theia-ChatNodeToolbar .theia-ChatNodeToolbarAction { + display: none; + align-items: center; + padding: 4px; + border-radius: 5px; +} +.theia-ChatNode:hover .theia-ChatNodeToolbar .theia-ChatNodeToolbarAction { + display: inline-block; +} +.theia-ChatNodeToolbar .theia-ChatNodeToolbarAction:hover { + cursor: pointer; + background-color: var(--theia-toolbar-hoverBackground); +} + +.theia-ChatNode { + line-height: 1.3rem; +} + +.theia-ChatNode ul, +.theia-ChatNode ol { + padding-inline-start: 1rem; +} + +.theia-ChatNode li > p { + margin-top: 0; + margin-bottom: 0; +} + +.theia-ChatNode .theia-CodeWrapper { + padding: 0.5em; + background-color: var(--theia-editor-background); + border-radius: 6px; + border: var(--theia-border-width) solid var(--theia-checkbox-border); + font-size: var(--theia-code-font-size); +} + +.chat-input-widget { + align-items: flex-end; + display: flex; + flex-direction: column; +} + +.theia-ChatInput { + position: relative; + width: 100%; + box-sizing: border-box; + gap: 4px; +} + +.theia-ChatInput-Editor-Box { + margin-bottom: 2px; + padding: 10px; + height: auto; + display: flex; + flex-direction: column; + justify-content: flex-end; + overflow: hidden; +} + +.theia-ChatInput-Editor { + width: 100%; + height: auto; + border: var(--theia-border-width) solid var(--theia-dropdown-border); + border-radius: 4px; + display: flex; + flex-direction: column-reverse; + overflow: hidden; +} + +.theia-ChatInput-Editor:has(.monaco-editor.focused) { + border-color: var(--theia-focusBorder); +} + +.theia-ChatInput-Editor .monaco-editor { + display: flex; + width: 100%; + height: 100%; + overflow: hidden; + position: relative; +} + +.theia-ChatInput-Editor-Placeholder { + position: absolute; + top: -3px; + left: 19px; + right: 0; + bottom: 0; + display: flex; + align-items: center; + color: var(--theia-descriptionForeground); + pointer-events: none; + z-index: 10; + text-align: left; +} +.theia-ChatInput-Editor-Placeholder.hidden { + display: none; +} + +.theia-ChatInput-Editor .monaco-editor .margin, +.theia-ChatInput-Editor .monaco-editor .monaco-editor-background, +.theia-ChatInput-Editor .monaco-editor .inputarea.ime-input { + padding-left: 8px !important; +} + +.theia-ChatInputOptions { + position: absolute; + bottom: 31px; + right: 26px; + width: 10px; + height: 10px; +} + +.theia-ChatInputOptions .option { + width: 21px; + height: 21px; + margin-top: 2px; + display: inline-block; + box-sizing: border-box; + user-select: none; + background-repeat: no-repeat; + background-position: center; + border: var(--theia-border-width) solid transparent; + opacity: 0.7; + cursor: pointer; +} + +.theia-ChatInputOptions .option:hover { + opacity: 1; +} + +.theia-CodePartRenderer-root { + display: flex; + flex-direction: column; + gap: 2px; + border: 1px solid var(--theia-input-border); + border-radius: 4px; +} + +.theia-CodePartRenderer-left { + flex-grow: 1; +} + +.theia-CodePartRenderer-top { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 2px; + padding-right: 2px; +} + +.theia-CodePartRenderer-right .button { + margin-left: 2px; + width: 18px; + height: 18px; + padding: 2px; + border-radius: 5px; + cursor: pointer; +} +.theia-CodePartRenderer-right .button:hover { + background-color: var(--theia-toolbar-hoverBackground); +} + +.theia-CodePartRenderer-separator { + width: 100%; + height: 1px; + background-color: var(--theia-input-border); +} + +.theia-toolCall { + font-weight: normal; + color: var(--theia-descriptionForeground); + line-height: 20px; + margin-top: 13px; + margin-bottom: 13px; + cursor: pointer; +} + +.theia-toolCall .fa, +.theia-toolCall details summary::marker { + color: var(--theia-button-background); +} + +.theia-toolCall details pre { + cursor: text; + line-height: 1rem; + margin-top: 0; + margin-bottom: 0; + padding: 6px; + background-color: var(--theia-editor-background); + overflow: auto; +} + +.theia-ResponseNode-ProgressMessage { + font-weight: normal; + color: var(--theia-descriptionForeground); + line-height: 20px; + margin-bottom: 6px; +} + +.theia-ResponseNode-ProgressMessage .inProgress { + color: var(--theia-progressBar-background); +} +.theia-ResponseNode-ProgressMessage .completed { + color: var(--theia-successBackground); +} +.theia-ResponseNode-ProgressMessage .failed { + color: var(--theia-errorForeground); +} + +.spinner { + display: inline-block; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.theia-ChatPart-Error { + display: flex; + flex-direction: row; + gap: 0.5em; + color: var(--theia-errorForeground); +} + +.section-header { + font-weight: bold; + font-size: 16px; + margin-bottom: 10px; +} + +.section-title { + font-weight: bold; + font-size: 14px; + margin: 20px 0px; +} + +.disable-message { + font-size: 12px; + line-height: 1.6; + padding: 15px; +} + +.section-content p { + margin: 10px 0; +} + +.section-content a { + cursor: pointer; +} + +.section-content strong { + font-weight: bold; +} diff --git a/packages/ai-chat-ui/tsconfig.json b/packages/ai-chat-ui/tsconfig.json new file mode 100644 index 0000000000000..13d585dc94ad5 --- /dev/null +++ b/packages/ai-chat-ui/tsconfig.json @@ -0,0 +1,37 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-chat" + }, + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../editor" + }, + { + "path": "../editor-preview" + }, + { + "path": "../filesystem" + }, + { + "path": "../monaco" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/ai-chat/.eslintrc.js b/packages/ai-chat/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-chat/.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/README.md b/packages/ai-chat/README.md new file mode 100644 index 0000000000000..6f394ce95cc55 --- /dev/null +++ b/packages/ai-chat/README.md @@ -0,0 +1,30 @@ +
+ +
+ +theia-ext-logo + +

ECLIPSE THEIA - AI Chat EXTENSION

+ +
+ +
+ +## Description + +The `@theia/ai-chat` extension provides the concept of a language model chat to Theia. +It serves as the basis for `@theia/ai-chat-ui` to provide the Chat UI. + +## 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/package.json b/packages/ai-chat/package.json new file mode 100644 index 0000000000000..bc6843dc6fe42 --- /dev/null +++ b/packages/ai-chat/package.json @@ -0,0 +1,53 @@ +{ + "name": "@theia/ai-chat", + "version": "1.54.0", + "description": "Theia - AI Chat Extension", + "dependencies": { + "@theia/ai-core": "1.54.0", + "@theia/ai-history": "1.54.0", + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/workspace": "1.54.0", + "minimatch": "^5.1.0", + "tslib": "^2.6.2" + }, + "publishConfig": { + "access": "public" + }, + "main": "lib/common", + "theiaExtensions": [ + { + "frontend": "lib/browser/ai-chat-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/src/browser/ai-chat-frontend-module.ts b/packages/ai-chat/src/browser/ai-chat-frontend-module.ts new file mode 100644 index 0000000000000..396bcb7331a06 --- /dev/null +++ b/packages/ai-chat/src/browser/ai-chat-frontend-module.ts @@ -0,0 +1,93 @@ +// ***************************************************************************** +// 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 { Agent, AgentService, AIVariableContribution } from '@theia/ai-core/lib/common'; +import { bindContributionProvider } from '@theia/core'; +import { FrontendApplicationContribution, PreferenceContribution } from '@theia/core/lib/browser'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { + ChatAgent, + ChatAgentService, + ChatAgentServiceImpl, + ChatRequestParser, + ChatRequestParserImpl, + ChatService, + DefaultChatAgentId +} from '../common'; +import { ChatAgentsVariableContribution } from '../common/chat-agents-variable-contribution'; +import { CommandChatAgent } from '../common/command-chat-agents'; +import { CustomChatAgent } from '../common/custom-chat-agent'; +import { OrchestratorChatAgent, OrchestratorChatAgentId } from '../common/orchestrator-chat-agent'; +import { DefaultResponseContentFactory, DefaultResponseContentMatcherProvider, ResponseContentMatcherProvider } from '../common/response-content-matcher'; +import { UniversalChatAgent } from '../common/universal-chat-agent'; +import { aiChatPreferences } from './ai-chat-preferences'; +import { AICustomAgentsFrontendApplicationContribution } from './custom-agent-frontend-application-contribution'; +import { FrontendChatServiceImpl } from './frontend-chat-service'; +import { CustomAgentFactory } from './custom-agent-factory'; + +export default new ContainerModule(bind => { + bindContributionProvider(bind, Agent); + bindContributionProvider(bind, ChatAgent); + + bind(ChatAgentServiceImpl).toSelf().inSingletonScope(); + bind(ChatAgentService).toService(ChatAgentServiceImpl); + bind(DefaultChatAgentId).toConstantValue({ id: OrchestratorChatAgentId }); + + bindContributionProvider(bind, ResponseContentMatcherProvider); + bind(DefaultResponseContentMatcherProvider).toSelf().inSingletonScope(); + bind(ResponseContentMatcherProvider).toService(DefaultResponseContentMatcherProvider); + bind(DefaultResponseContentFactory).toSelf().inSingletonScope(); + + bind(AIVariableContribution).to(ChatAgentsVariableContribution).inSingletonScope(); + + bind(ChatRequestParserImpl).toSelf().inSingletonScope(); + bind(ChatRequestParser).toService(ChatRequestParserImpl); + + bind(FrontendChatServiceImpl).toSelf().inSingletonScope(); + bind(ChatService).toService(FrontendChatServiceImpl); + + bind(OrchestratorChatAgent).toSelf().inSingletonScope(); + bind(Agent).toService(OrchestratorChatAgent); + bind(ChatAgent).toService(OrchestratorChatAgent); + + bind(UniversalChatAgent).toSelf().inSingletonScope(); + bind(Agent).toService(UniversalChatAgent); + bind(ChatAgent).toService(UniversalChatAgent); + + bind(CommandChatAgent).toSelf().inSingletonScope(); + bind(Agent).toService(CommandChatAgent); + bind(ChatAgent).toService(CommandChatAgent); + + bind(PreferenceContribution).toConstantValue({ schema: aiChatPreferences }); + + bind(CustomChatAgent).toSelf(); + bind(CustomAgentFactory).toFactory( + ctx => (id: string, name: string, description: string, prompt: string, defaultLLM: string) => { + const agent = ctx.container.get(CustomChatAgent); + agent.id = id; + agent.name = name; + agent.description = description; + agent.prompt = prompt; + agent.languageModelRequirements = [{ + purpose: 'chat', + identifier: defaultLLM, + }]; + ctx.container.get(ChatAgentService).registerChatAgent(agent); + ctx.container.get(AgentService).registerAgent(agent); + return agent; + }); + bind(FrontendApplicationContribution).to(AICustomAgentsFrontendApplicationContribution).inSingletonScope(); +}); diff --git a/packages/ai-chat/src/browser/ai-chat-preferences.ts b/packages/ai-chat/src/browser/ai-chat-preferences.ts new file mode 100644 index 0000000000000..11dee619e4ff0 --- /dev/null +++ b/packages/ai-chat/src/browser/ai-chat-preferences.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// 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 { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/browser/ai-core-preferences'; +import { PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; + +export const DEFAULT_CHAT_AGENT_PREF = 'ai-features.chat.defaultChatAgent'; + +export const aiChatPreferences: PreferenceSchema = { + type: 'object', + properties: { + [DEFAULT_CHAT_AGENT_PREF]: { + type: 'string', + description: 'Optional: of the Chat Agent that shall be invoked, if no agent is explicitly mentioned with @ in the user query.\ + If no Default Agent is configured, Theia´s defaults will be applied.', + title: AI_CORE_PREFERENCES_TITLE, + } + } +}; diff --git a/packages/ai-chat/src/browser/custom-agent-factory.ts b/packages/ai-chat/src/browser/custom-agent-factory.ts new file mode 100644 index 0000000000000..9fe67f88e723e --- /dev/null +++ b/packages/ai-chat/src/browser/custom-agent-factory.ts @@ -0,0 +1,20 @@ +// ***************************************************************************** +// 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 { CustomChatAgent } from '../common'; + +export const CustomAgentFactory = Symbol('CustomAgentFactory'); +export type CustomAgentFactory = (id: string, name: string, description: string, prompt: string, defaultLLM: string) => CustomChatAgent; diff --git a/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts b/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts new file mode 100644 index 0000000000000..4c67a14ab508c --- /dev/null +++ b/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts @@ -0,0 +1,73 @@ +// ***************************************************************************** +// 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 { AgentService, CustomAgentDescription, PromptCustomizationService } from '@theia/ai-core'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ChatAgentService } from '../common'; +import { CustomAgentFactory } from './custom-agent-factory'; + +@injectable() +export class AICustomAgentsFrontendApplicationContribution implements FrontendApplicationContribution { + @inject(CustomAgentFactory) + protected readonly customAgentFactory: CustomAgentFactory; + + @inject(PromptCustomizationService) + protected readonly customizationService: PromptCustomizationService; + + @inject(AgentService) + private readonly agentService: AgentService; + + @inject(ChatAgentService) + private readonly chatAgentService: ChatAgentService; + + private knownCustomAgents: Map = new Map(); + onStart(): void { + this.customizationService?.getCustomAgents().then(customAgents => { + customAgents.forEach(agent => { + this.customAgentFactory(agent.id, agent.name, agent.description, agent.prompt, agent.defaultLLM); + this.knownCustomAgents.set(agent.id, agent); + }); + }).catch(e => { + console.error('Failed to load custom agents', e); + }); + this.customizationService?.onDidChangeCustomAgents(() => { + this.customizationService?.getCustomAgents().then(customAgents => { + const customAgentsToAdd = customAgents.filter(agent => + !this.knownCustomAgents.has(agent.id) || !CustomAgentDescription.equals(this.knownCustomAgents.get(agent.id)!, agent)); + const customAgentIdsToRemove = [...this.knownCustomAgents.values()].filter(agent => + !customAgents.find(a => CustomAgentDescription.equals(a, agent))).map(a => a.id); + + // delete first so we don't have to deal with the case where we add and remove the same agentId + customAgentIdsToRemove.forEach(id => { + this.chatAgentService.unregisterChatAgent(id); + this.agentService.unregisterAgent(id); + this.knownCustomAgents.delete(id); + }); + customAgentsToAdd + .forEach(agent => { + this.customAgentFactory(agent.id, agent.name, agent.description, agent.prompt, agent.defaultLLM); + this.knownCustomAgents.set(agent.id, agent); + }); + }).catch(e => { + console.error('Failed to load custom agents', e); + }); + }); + } + + onStop(): void { + } +} diff --git a/packages/ai-chat/src/browser/frontend-chat-service.ts b/packages/ai-chat/src/browser/frontend-chat-service.ts new file mode 100644 index 0000000000000..10e7dde4a1d46 --- /dev/null +++ b/packages/ai-chat/src/browser/frontend-chat-service.ts @@ -0,0 +1,66 @@ +// ***************************************************************************** +// 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 { ChatAgent, ChatServiceImpl, ParsedChatRequest } from '../common'; +import { PreferenceService } from '@theia/core/lib/browser'; +import { DEFAULT_CHAT_AGENT_PREF } from './ai-chat-preferences'; + +@injectable() +export class FrontendChatServiceImpl extends ChatServiceImpl { + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + protected override getAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined { + const agentPart = this.getMentionedAgent(parsedRequest); + if (agentPart) { + return this.chatAgentService.getAgent(agentPart.agentId); + } + + const configuredDefaultChatAgent = this.getConfiguredDefaultChatAgent(); + if (configuredDefaultChatAgent) { + return configuredDefaultChatAgent; + } + + if (this.defaultChatAgentId) { + const defaultAgent = this.chatAgentService.getAgent(this.defaultChatAgentId.id); + // the default agent could be disabled + if (defaultAgent) { + return defaultAgent; + } + } + + // check whether "Universal" is available + const universalAgent = this.chatAgentService.getAgent('Universal'); + if (universalAgent) { + return universalAgent; + } + + this.logger.warn('No default chat agent is configured or available and the "Universal" Chat Agent is unavailable too. Falling back to first registered agent.'); + + return this.chatAgentService.getAgents()[0] ?? undefined; + } + + protected getConfiguredDefaultChatAgent(): ChatAgent | undefined { + const configuredDefaultChatAgentId = this.preferenceService.get(DEFAULT_CHAT_AGENT_PREF, undefined); + const configuredDefaultChatAgent = configuredDefaultChatAgentId ? this.chatAgentService.getAgent(configuredDefaultChatAgentId) : undefined; + if (configuredDefaultChatAgentId && !configuredDefaultChatAgent) { + this.logger.warn(`The configured default chat agent with id '${configuredDefaultChatAgentId}' does not exist.`); + } + return configuredDefaultChatAgent; + } +} diff --git a/packages/ai-chat/src/common/chat-agent-service.ts b/packages/ai-chat/src/common/chat-agent-service.ts new file mode 100644 index 0000000000000..7c01541d44be1 --- /dev/null +++ b/packages/ai-chat/src/common/chat-agent-service.ts @@ -0,0 +1,100 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatAgents.ts + +import { ContributionProvider, ILogger } from '@theia/core'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { ChatAgent } from './chat-agents'; +import { AgentService } from '@theia/ai-core'; + +export const ChatAgentService = Symbol('ChatAgentService'); +/** + * The ChatAgentService provides access to the available chat agents. + */ +export interface ChatAgentService { + /** + * Returns all available agents. + */ + getAgents(): ChatAgent[]; + /** + * Returns the specified agent, if available + */ + getAgent(id: string): ChatAgent | undefined; + /** + * Returns all agents, including disabled ones. + */ + getAllAgents(): ChatAgent[]; + + /** + * Allows to register a chat agent programmatically. + * @param agent the agent to register + */ + registerChatAgent(agent: ChatAgent): void; + + /** + * Allows to unregister a chat agent programmatically. + * @param agentId the agent id to unregister + */ + unregisterChatAgent(agentId: string): void; +} +@injectable() +export class ChatAgentServiceImpl implements ChatAgentService { + + @inject(ContributionProvider) @named(ChatAgent) + protected readonly agentContributions: ContributionProvider; + + @inject(ILogger) + protected logger: ILogger; + + @inject(AgentService) + protected agentService: AgentService; + + protected _agents: ChatAgent[] = []; + + protected get agents(): ChatAgent[] { + // We can't collect the contributions at @postConstruct because this will lead to a circular dependency + // with chat agents reusing the chat agent service (e.g. orchestrator) + return [...this.agentContributions.getContributions(), ...this._agents]; + } + + registerChatAgent(agent: ChatAgent): void { + this._agents.push(agent); + } + unregisterChatAgent(agentId: string): void { + this._agents = this._agents.filter(a => a.id !== agentId); + } + + getAgent(id: string): ChatAgent | undefined { + if (!this._agentIsEnabled(id)) { + return undefined; + } + return this.getAgents().find(agent => agent.id === id); + } + getAgents(): ChatAgent[] { + return this.agents.filter(a => this._agentIsEnabled(a.id)); + } + getAllAgents(): ChatAgent[] { + return this.agents; + } + + private _agentIsEnabled(id: string): boolean { + return this.agentService.isEnabled(id); + } +} diff --git a/packages/ai-chat/src/common/chat-agents-variable-contribution.ts b/packages/ai-chat/src/common/chat-agents-variable-contribution.ts new file mode 100644 index 0000000000000..72344d166fb54 --- /dev/null +++ b/packages/ai-chat/src/common/chat-agents-variable-contribution.ts @@ -0,0 +1,81 @@ +// ***************************************************************************** +// 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 { MaybePromise } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { + AIVariable, + AIVariableContext, + AIVariableContribution, + AIVariableResolutionRequest, + AIVariableResolver, + AIVariableService, + ResolvedAIVariable +} from '@theia/ai-core'; +import { ChatAgentService } from './chat-agent-service'; + +export const CHAT_AGENTS_VARIABLE: AIVariable = { + id: 'chatAgents', + name: 'chatAgents', + description: 'Returns the list of chat agents available in the system' +}; + +export interface ChatAgentDescriptor { + id: string; + name: string; + description: string; +} + +@injectable() +export class ChatAgentsVariableContribution implements AIVariableContribution, AIVariableResolver { + + @inject(ChatAgentService) + protected readonly agents: ChatAgentService; + + registerVariables(service: AIVariableService): void { + service.registerResolver(CHAT_AGENTS_VARIABLE, this); + } + + canResolve(request: AIVariableResolutionRequest, _context: AIVariableContext): MaybePromise { + if (request.variable.name === CHAT_AGENTS_VARIABLE.name) { + return 1; + } + return -1; + } + + async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise { + if (request.variable.name === CHAT_AGENTS_VARIABLE.name) { + return this.resolveAgentsVariable(request); + } + } + + resolveAgentsVariable(_request: AIVariableResolutionRequest): ResolvedAIVariable { + const agents = this.agents.getAgents().map(agent => ({ + id: agent.id, + name: agent.name, + description: agent.description + })); + const value = agents.map(agent => prettyPrintInMd(agent)).join('\n'); + return { variable: CHAT_AGENTS_VARIABLE, value }; + } +} + +function prettyPrintInMd(agent: { id: string; name: string; description: string; }): string { + return `- ${agent.id} + - *ID*: ${agent.id} + - *Name*: ${agent.name} + - *Description*: ${agent.description.replace(/\n/g, ' ')}`; +} + diff --git a/packages/ai-chat/src/common/chat-agents.ts b/packages/ai-chat/src/common/chat-agents.ts new file mode 100644 index 0000000000000..5a2451a5f2bdb --- /dev/null +++ b/packages/ai-chat/src/common/chat-agents.ts @@ -0,0 +1,402 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatAgents.ts + +import { + CommunicationRecordingService, + getTextOfResponse, + LanguageModel, + LanguageModelRequirement, + LanguageModelResponse, + LanguageModelStreamResponse, + PromptService, + ResolvedPromptTemplate, + ToolRequest, +} from '@theia/ai-core'; +import { + Agent, + isLanguageModelStreamResponse, + isLanguageModelTextResponse, + LanguageModelRegistry, + LanguageModelStreamResponsePart, + MessageActor, +} from '@theia/ai-core/lib/common'; +import { CancellationToken, CancellationTokenSource, ContributionProvider, ILogger, isArray } from '@theia/core'; +import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; +import { ChatAgentService } from './chat-agent-service'; +import { + ChatModel, + ChatRequestModel, + ChatRequestModelImpl, + ChatResponseContent, + ErrorChatResponseContentImpl, + MarkdownChatResponseContentImpl, + ToolCallChatResponseContentImpl +} from './chat-model'; +import { findFirstMatch, parseContents } from './parse-contents'; +import { DefaultResponseContentFactory, ResponseContentMatcher, ResponseContentMatcherProvider } from './response-content-matcher'; + +/** + * A conversation consists of a sequence of ChatMessages. + * Each ChatMessage is either a user message, AI message or a system message. + * + * For now we only support text based messages. + */ +export interface ChatMessage { + actor: MessageActor; + type: 'text'; + query: string; +} + +/** + * System message content, enriched with function descriptions. + */ +export interface SystemMessageDescription { + text: string; + /** All functions references in the system message. */ + functionDescriptions?: Map; +} +export namespace SystemMessageDescription { + export function fromResolvedPromptTemplate(resolvedPrompt: ResolvedPromptTemplate): SystemMessageDescription { + return { + text: resolvedPrompt.text, + functionDescriptions: resolvedPrompt.functionDescriptions + }; + } +} + +/** + * The location from where an chat agent may be invoked. + * Based on the location, a different context may be available. + */ +export enum ChatAgentLocation { + Panel = 'panel', + Terminal = 'terminal', + Notebook = 'notebook', + Editor = 'editor' +} + +export namespace ChatAgentLocation { + export const ALL: ChatAgentLocation[] = [ChatAgentLocation.Panel, ChatAgentLocation.Terminal, ChatAgentLocation.Notebook, ChatAgentLocation.Editor]; + + export function fromRaw(value: string): ChatAgentLocation { + switch (value) { + case 'panel': return ChatAgentLocation.Panel; + case 'terminal': return ChatAgentLocation.Terminal; + case 'notebook': return ChatAgentLocation.Notebook; + case 'editor': return ChatAgentLocation.Editor; + } + return ChatAgentLocation.Panel; + } +} + +export const ChatAgent = Symbol('ChatAgent'); +/** + * A chat agent is a specialized agent with a common interface for its invocation. + */ +export interface ChatAgent extends Agent { + locations: ChatAgentLocation[]; + iconClass?: string; + invoke(request: ChatRequestModelImpl, chatAgentService?: ChatAgentService): Promise; +} + +@injectable() +export abstract class AbstractChatAgent { + @inject(LanguageModelRegistry) protected languageModelRegistry: LanguageModelRegistry; + @inject(ILogger) protected logger: ILogger; + @inject(CommunicationRecordingService) protected recordingService: CommunicationRecordingService; + @inject(PromptService) protected promptService: PromptService; + + @inject(ContributionProvider) @named(ResponseContentMatcherProvider) + protected contentMatcherProviders: ContributionProvider; + protected contentMatchers: ResponseContentMatcher[] = []; + + @inject(DefaultResponseContentFactory) + protected defaultContentFactory: DefaultResponseContentFactory; + + constructor( + public id: string, + public languageModelRequirements: LanguageModelRequirement[], + protected defaultLanguageModelPurpose: string, + public iconClass: string = 'codicon codicon-copilot', + public locations: ChatAgentLocation[] = ChatAgentLocation.ALL, + public tags: String[] = ['Chat'], + public defaultLogging: boolean = true) { + } + + @postConstruct() + init(): void { + this.contentMatchers = this.contentMatcherProviders.getContributions().flatMap(provider => provider.matchers); + } + + async invoke(request: ChatRequestModelImpl): Promise { + try { + const languageModel = await this.getLanguageModel(this.defaultLanguageModelPurpose); + if (!languageModel) { + throw new Error('Couldn\'t find a matching language model. Please check your setup!'); + } + const messages = await this.getMessages(request.session); + if (this.defaultLogging) { + this.recordingService.recordRequest({ + agentId: this.id, + sessionId: request.session.id, + timestamp: Date.now(), + requestId: request.id, + request: request.request.text, + messages + }); + } + + const systemMessageDescription = await this.getSystemMessageDescription(); + const tools: Map = new Map(); + if (systemMessageDescription) { + const systemMsg: ChatMessage = { + actor: 'system', + type: 'text', + query: systemMessageDescription.text + }; + // insert system message at the beginning of the request messages + messages.unshift(systemMsg); + systemMessageDescription.functionDescriptions?.forEach((tool, id) => { + tools.set(id, tool); + }); + } + this.getTools(request)?.forEach(tool => tools.set(tool.id, tool)); + + const cancellationToken = new CancellationTokenSource(); + request.response.onDidChange(() => { + if (request.response.isCanceled) { + cancellationToken.cancel(); + } + }); + + const languageModelResponse = await this.callLlm( + languageModel, + messages, + tools.size > 0 ? Array.from(tools.values()) : undefined, + cancellationToken.token + ); + await this.addContentsToResponse(languageModelResponse, request); + request.response.complete(); + if (this.defaultLogging) { + this.recordingService.recordResponse({ + agentId: this.id, + sessionId: request.session.id, + timestamp: Date.now(), + requestId: request.response.requestId, + response: request.response.response.asString() + }); + } + } catch (e) { + this.handleError(request, e); + } + } + + protected parseContents(text: string): ChatResponseContent[] { + return parseContents( + text, + this.contentMatchers, + this.defaultContentFactory?.create.bind(this.defaultContentFactory) + ); + }; + + protected handleError(request: ChatRequestModelImpl, error: Error): void { + request.response.response.addContent(new ErrorChatResponseContentImpl(error)); + request.response.error(error); + } + + protected getLanguageModelSelector(languageModelPurpose: string): LanguageModelRequirement { + return this.languageModelRequirements.find(req => req.purpose === languageModelPurpose)!; + } + + protected async getLanguageModel(languageModelPurpose: string): Promise { + return this.selectLanguageModel(this.getLanguageModelSelector(languageModelPurpose)); + } + + protected async selectLanguageModel(selector: LanguageModelRequirement): Promise { + const languageModel = await this.languageModelRegistry.selectLanguageModel({ agent: this.id, ...selector }); + if (!languageModel) { + throw new Error('Couldn\'t find a language model. Please check your setup!'); + } + return languageModel; + } + + protected abstract getSystemMessageDescription(): Promise; + + protected async getMessages( + model: ChatModel, includeResponseInProgress = false + ): Promise { + const requestMessages = model.getRequests().flatMap(request => { + const messages: ChatMessage[] = []; + const text = request.message.parts.map(part => part.promptText).join(''); + messages.push({ + actor: 'user', + type: 'text', + query: text, + }); + if (request.response.isComplete || includeResponseInProgress) { + messages.push({ + actor: 'ai', + type: 'text', + query: request.response.response.asString(), + }); + } + return messages; + }); + + return requestMessages; + } + + /** + * @returns the list of tools used by this agent, or undefined if none is needed. + */ + protected getTools(request: ChatRequestModel): ToolRequest[] | undefined { + return request.message.toolRequests.size > 0 + ? [...request.message.toolRequests.values()] + : undefined; + } + + protected async callLlm( + languageModel: LanguageModel, + messages: ChatMessage[], + tools: ToolRequest[] | undefined, + token: CancellationToken + ): Promise { + const settings = this.getLlmSettings(); + const languageModelResponse = languageModel.request({ + messages, + tools, + settings, + }, token); + return languageModelResponse; + } + + /** + * @returns the settings, such as `temperature`, to be used in all language model requests. Returns `undefined` by default. + */ + protected getLlmSettings(): { [key: string]: unknown; } | undefined { + return undefined; + } + + protected abstract addContentsToResponse(languageModelResponse: LanguageModelResponse, request: ChatRequestModelImpl): Promise; +} + +@injectable() +export abstract class AbstractTextToModelParsingChatAgent extends AbstractChatAgent { + + protected async addContentsToResponse(languageModelResponse: LanguageModelResponse, request: ChatRequestModelImpl): Promise { + const responseAsText = await getTextOfResponse(languageModelResponse); + const parsedCommand = await this.parseTextResponse(responseAsText); + const content = this.createResponseContent(parsedCommand, request); + request.response.response.addContent(content); + } + + protected abstract parseTextResponse(text: string): Promise; + + protected abstract createResponseContent(parsedModel: T, request: ChatRequestModelImpl): ChatResponseContent; +} + +@injectable() +export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent { + + protected override async addContentsToResponse(languageModelResponse: LanguageModelResponse, request: ChatRequestModelImpl): Promise { + if (isLanguageModelTextResponse(languageModelResponse)) { + const contents = this.parseContents(languageModelResponse.text); + request.response.response.addContents(contents); + request.response.complete(); + if (this.defaultLogging) { + this.recordingService.recordResponse({ + agentId: this.id, + sessionId: request.session.id, + timestamp: Date.now(), + requestId: request.response.requestId, + response: request.response.response.asString() + + }); + } + return; + } + if (isLanguageModelStreamResponse(languageModelResponse)) { + await this.addStreamResponse(languageModelResponse, request); + request.response.complete(); + if (this.defaultLogging) { + this.recordingService.recordResponse({ + agentId: this.id, + sessionId: request.session.id, + timestamp: Date.now(), + requestId: request.response.requestId, + response: request.response.response.asString() + }); + } + return; + } + this.logger.error( + 'Received unknown response in agent. Return response as text' + ); + request.response.response.addContent( + new MarkdownChatResponseContentImpl( + JSON.stringify(languageModelResponse) + ) + ); + } + + protected async addStreamResponse(languageModelResponse: LanguageModelStreamResponse, request: ChatRequestModelImpl): Promise { + for await (const token of languageModelResponse.stream) { + const newContents = this.parse(token, request.response.response.content); + if (isArray(newContents)) { + request.response.response.addContents(newContents); + } else { + request.response.response.addContent(newContents); + } + + const lastContent = request.response.response.content.pop(); + if (lastContent === undefined) { + return; + } + const text = lastContent.asString?.(); + if (text === undefined) { + return; + } + + const result: ChatResponseContent[] = findFirstMatch(this.contentMatchers, text) ? this.parseContents(text) : []; + if (result.length > 0) { + request.response.response.addContents(result); + } else { + request.response.response.addContent(lastContent); + } + } + } + + protected parse(token: LanguageModelStreamResponsePart, previousContent: ChatResponseContent[]): ChatResponseContent | ChatResponseContent[] { + const content = token.content; + // eslint-disable-next-line no-null/no-null + if (content !== undefined && content !== null) { + return this.defaultContentFactory.create(content); + } + const toolCalls = token.tool_calls; + if (toolCalls !== undefined) { + const toolCallContents = toolCalls.map(toolCall => + new ToolCallChatResponseContentImpl(toolCall.id, toolCall.function?.name, toolCall.function?.arguments, toolCall.finished, toolCall.result)); + return toolCallContents; + } + return this.defaultContentFactory.create(''); + } + +} diff --git a/packages/ai-chat/src/common/chat-model.ts b/packages/ai-chat/src/common/chat-model.ts new file mode 100644 index 0000000000000..0decfb284da35 --- /dev/null +++ b/packages/ai-chat/src/common/chat-model.ts @@ -0,0 +1,800 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts + +import { Command, Emitter, Event, generateUuid, URI } from '@theia/core'; +import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering'; +import { Position } from '@theia/core/shared/vscode-languageserver-protocol'; +import { ChatAgentLocation } from './chat-agents'; +import { ParsedChatRequest } from './parsed-chat-request'; + +/********************** + * INTERFACES AND TYPE GUARDS + **********************/ + +export type ChatChangeEvent = + | ChatAddRequestEvent + | ChatAddResponseEvent + | ChatRemoveRequestEvent; + +export interface ChatAddRequestEvent { + kind: 'addRequest'; + request: ChatRequestModel; +} + +export interface ChatAddResponseEvent { + kind: 'addResponse'; + response: ChatResponseModel; +} + +export type ChatRequestRemovalReason = 'removal' | 'resend' | 'adoption'; + +export interface ChatRemoveRequestEvent { + kind: 'removeRequest'; + requestId: string; + responseId?: string; + reason: ChatRequestRemovalReason; +} + +export interface ChatModel { + readonly onDidChange: Event; + readonly id: string; + readonly location: ChatAgentLocation; + getRequests(): ChatRequestModel[]; + isEmpty(): boolean; +} + +export interface ChatRequest { + readonly text: string; + readonly displayText?: string; +} + +export interface ChatRequestModel { + readonly id: string; + readonly session: ChatModel; + readonly request: ChatRequest; + readonly response: ChatResponseModel; + readonly message: ParsedChatRequest; + readonly agentId?: string; + readonly data?: { [key: string]: unknown }; +} + +export interface ChatProgressMessage { + kind: 'progressMessage'; + id: string; + status: 'inProgress' | 'completed' | 'failed'; + content: string; +} + +export interface ChatResponseContent { + kind: string; + /** + * Represents the content as a string. Returns `undefined` if the content + * is purely informational and/or visual and should not be included in the overall + * representation of the response. + */ + asString?(): string | undefined; + merge?(nextChatResponseContent: ChatResponseContent): boolean; +} + +export namespace ChatResponseContent { + export function is(obj: unknown): obj is ChatResponseContent { + return !!( + obj && + typeof obj === 'object' && + 'kind' in obj && + typeof (obj as { kind: unknown }).kind === 'string' + ); + } + export function hasAsString( + obj: ChatResponseContent + ): obj is Required> & ChatResponseContent { + return typeof obj.asString === 'function'; + } + export function hasMerge( + obj: ChatResponseContent + ): obj is Required> & ChatResponseContent { + return typeof obj.merge === 'function'; + } +} + +export interface TextChatResponseContent + extends Required { + kind: 'text'; + content: string; +} + +export interface ErrorChatResponseContent extends ChatResponseContent { + kind: 'error'; + error: Error; +} + +export interface MarkdownChatResponseContent + extends Required { + kind: 'markdownContent'; + content: MarkdownString; +} + +export interface CodeChatResponseContent + extends ChatResponseContent { + kind: 'code'; + code: string; + language?: string; + location?: Location; +} + +export interface HorizontalLayoutChatResponseContent extends Required { + kind: 'horizontal'; + content: ChatResponseContent[]; +} + +export interface ToolCallChatResponseContent extends Required { + kind: 'toolCall'; + id?: string; + name?: string; + arguments?: string; + finished: boolean; + result?: string; +} + +export interface Location { + uri: URI; + position: Position; +} +export namespace Location { + export function is(obj: unknown): obj is Location { + return !!obj && typeof obj === 'object' && + 'uri' in obj && (obj as { uri: unknown }).uri instanceof URI && + 'position' in obj && Position.is((obj as { position: unknown }).position); + } +} + +export interface CustomCallback { + label: string; + callback: () => Promise; +} + +/** + * A command chat response content represents a command that is offered to the user for execution. + * It either refers to an already registered Theia command or provides a custom callback. + * If both are given, the custom callback will be preferred. + */ +export interface CommandChatResponseContent extends ChatResponseContent { + kind: 'command'; + command?: Command; + customCallback?: CustomCallback; + arguments?: unknown[]; +} + +/** + * An informational chat response content represents a message that is purely informational and should not be included in the overall representation of the response. + */ +export interface InformationalChatResponseContent extends ChatResponseContent { + kind: 'informational'; + content: MarkdownString; +} + +export namespace TextChatResponseContent { + export function is(obj: unknown): obj is TextChatResponseContent { + return ( + ChatResponseContent.is(obj) && + obj.kind === 'text' && + 'content' in obj && + typeof (obj as { content: unknown }).content === 'string' + ); + } +} + +export namespace MarkdownChatResponseContent { + export function is(obj: unknown): obj is MarkdownChatResponseContent { + return ( + ChatResponseContent.is(obj) && + obj.kind === 'markdownContent' && + 'content' in obj && + MarkdownString.is((obj as { content: unknown }).content) + ); + } +} + +export namespace InformationalChatResponseContent { + export function is(obj: unknown): obj is InformationalChatResponseContent { + return ( + ChatResponseContent.is(obj) && + obj.kind === 'informational' && + 'content' in obj && + MarkdownString.is((obj as { content: unknown }).content) + ); + } +} + +export namespace CommandChatResponseContent { + export function is(obj: unknown): obj is CommandChatResponseContent { + return ( + ChatResponseContent.is(obj) && + obj.kind === 'command' && + 'command' in obj && + Command.is((obj as { command: unknown }).command) + ); + } +} + +export namespace CodeChatResponseContent { + export function is(obj: unknown): obj is CodeChatResponseContent { + return ( + ChatResponseContent.is(obj) && + obj.kind === 'code' && + 'code' in obj && + typeof (obj as { code: unknown }).code === 'string' + ); + } +} + +export namespace HorizontalLayoutChatResponseContent { + export function is( + obj: unknown + ): obj is HorizontalLayoutChatResponseContent { + return ( + ChatResponseContent.is(obj) && + obj.kind === 'horizontal' && + 'content' in obj && + Array.isArray((obj as { content: unknown }).content) && + (obj as { content: unknown[] }).content.every( + ChatResponseContent.is + ) + ); + } +} + +export namespace ToolCallChatResponseContent { + export function is(obj: unknown): obj is ToolCallChatResponseContent { + return ChatResponseContent.is(obj) && obj.kind === 'toolCall'; + } +} + +export namespace ErrorChatResponseContent { + export function is(obj: unknown): obj is ErrorChatResponseContent { + return ( + ChatResponseContent.is(obj) && + obj.kind === 'error' && + 'error' in obj && + obj.error instanceof Error + ); + } +} + +export interface ChatResponse { + readonly content: ChatResponseContent[]; + asString(): string; +} + +export interface ChatResponseModel { + readonly onDidChange: Event; + readonly id: string; + readonly requestId: string; + readonly progressMessages: ChatProgressMessage[]; + readonly response: ChatResponse; + readonly isComplete: boolean; + readonly isCanceled: boolean; + readonly isError: boolean; + readonly agentId?: string + readonly errorObject?: Error; +} + +/********************** + * Implementations + **********************/ + +export class ChatModelImpl implements ChatModel { + protected readonly _onDidChangeEmitter = new Emitter(); + onDidChange: Event = this._onDidChangeEmitter.event; + + protected _requests: ChatRequestModelImpl[]; + protected _id: string; + + constructor(public readonly location = ChatAgentLocation.Panel) { + // TODO accept serialized data as a parameter to restore a previously saved ChatModel + this._requests = []; + this._id = generateUuid(); + } + + getRequests(): ChatRequestModelImpl[] { + return this._requests; + } + + get id(): string { + return this._id; + } + + addRequest(parsedChatRequest: ParsedChatRequest, agentId?: string): ChatRequestModelImpl { + const requestModel = new ChatRequestModelImpl(this, parsedChatRequest, agentId); + this._requests.push(requestModel); + this._onDidChangeEmitter.fire({ + kind: 'addRequest', + request: requestModel, + }); + return requestModel; + } + + isEmpty(): boolean { + return this._requests.length === 0; + } +} + +export class ChatRequestModelImpl implements ChatRequestModel { + protected readonly _id: string; + protected _session: ChatModel; + protected _request: ChatRequest; + protected _response: ChatResponseModelImpl; + protected _agentId?: string; + protected _data: { [key: string]: unknown }; + + constructor(session: ChatModel, public readonly message: ParsedChatRequest, agentId?: string, + data: { [key: string]: unknown } = {}) { + // TODO accept serialized data as a parameter to restore a previously saved ChatRequestModel + this._request = message.request; + this._id = generateUuid(); + this._session = session; + this._response = new ChatResponseModelImpl(this._id, agentId); + this._agentId = agentId; + this._data = data; + } + + get data(): { [key: string]: unknown } | undefined { + return this._data; + } + + addData(key: string, value: unknown): void { + this._data[key] = value; + } + + getDataByKey(key: string): unknown { + return this._data[key]; + } + + get id(): string { + return this._id; + } + + get session(): ChatModel { + return this._session; + } + + get request(): ChatRequest { + return this._request; + } + + get response(): ChatResponseModelImpl { + return this._response; + } + + get agentId(): string | undefined { + return this._agentId; + } +} + +export class ErrorChatResponseContentImpl implements ErrorChatResponseContent { + readonly kind = 'error'; + protected _error: Error; + constructor(error: Error) { + this._error = error; + } + get error(): Error { + return this._error; + } + asString(): string | undefined { + return undefined; + } +} + +export class TextChatResponseContentImpl implements TextChatResponseContent { + readonly kind = 'text'; + protected _content: string; + + constructor(content: string) { + this._content = content; + } + + get content(): string { + return this._content; + } + + asString(): string { + return this._content; + } + + merge(nextChatResponseContent: TextChatResponseContent): boolean { + this._content += nextChatResponseContent.content; + return true; + } +} + +export class MarkdownChatResponseContentImpl implements MarkdownChatResponseContent { + readonly kind = 'markdownContent'; + protected _content: MarkdownStringImpl = new MarkdownStringImpl(); + + constructor(content: string) { + this._content.appendMarkdown(content); + } + + get content(): MarkdownString { + return this._content; + } + + asString(): string { + return this._content.value; + } + + merge(nextChatResponseContent: MarkdownChatResponseContent): boolean { + this._content.appendMarkdown(nextChatResponseContent.content.value); + return true; + } +} + +export class InformationalChatResponseContentImpl implements InformationalChatResponseContent { + readonly kind = 'informational'; + protected _content: MarkdownStringImpl; + + constructor(content: string) { + this._content = new MarkdownStringImpl(content); + } + + get content(): MarkdownString { + return this._content; + } + + asString(): string | undefined { + return undefined; + } + + merge(nextChatResponseContent: InformationalChatResponseContent): boolean { + this._content.appendMarkdown(nextChatResponseContent.content.value); + return true; + } +} + +export class CodeChatResponseContentImpl implements CodeChatResponseContent { + readonly kind = 'code'; + protected _code: string; + protected _language?: string; + protected _location?: Location; + + constructor(code: string, language?: string, location?: Location) { + this._code = code; + this._language = language; + this._location = location; + } + + get code(): string { + return this._code; + } + + get language(): string | undefined { + return this._language; + } + + get location(): Location | undefined { + return this._location; + } + + asString(): string { + return `\`\`\`${this._language ?? ''}\n${this._code}\n\`\`\``; + } + + merge(nextChatResponseContent: CodeChatResponseContent): boolean { + this._code += `${nextChatResponseContent.code}`; + return true; + } +} + +export class ToolCallChatResponseContentImpl implements ToolCallChatResponseContent { + readonly kind = 'toolCall'; + protected _id?: string; + protected _name?: string; + protected _arguments?: string; + protected _finished?: boolean; + protected _result?: string; + + constructor(id?: string, name?: string, arg_string?: string, finished?: boolean, result?: string) { + this._id = id; + this._name = name; + this._arguments = arg_string; + this._finished = finished; + this._result = result; + } + + get id(): string | undefined { + return this._id; + } + + get name(): string | undefined { + return this._name; + } + + get arguments(): string | undefined { + return this._arguments; + } + + get finished(): boolean { + return this._finished === undefined ? false : this._finished; + } + get result(): string | undefined { + return this._result; + } + + asString(): string { + return `Tool call: ${this._name}(${this._arguments ?? ''})`; + } + merge(nextChatResponseContent: ToolCallChatResponseContent): boolean { + if (nextChatResponseContent.id === this.id) { + this._finished = nextChatResponseContent.finished; + this._result = nextChatResponseContent.result; + return true; + } + if (nextChatResponseContent.name !== undefined) { + return false; + } + if (nextChatResponseContent.arguments === undefined) { + return false; + } + this._arguments += `${nextChatResponseContent.arguments}`; + return true; + } +} + +export const COMMAND_CHAT_RESPONSE_COMMAND: Command = { + id: 'ai-chat.command-chat-response.generic' +}; +export class CommandChatResponseContentImpl implements CommandChatResponseContent { + readonly kind = 'command'; + + constructor(public command?: Command, public customCallback?: CustomCallback, protected args?: unknown[]) { + } + + get arguments(): unknown[] { + return this.args ?? []; + } + + asString(): string { + return this.command?.id || this.customCallback?.label || 'command'; + } +} + +export class HorizontalLayoutChatResponseContentImpl implements HorizontalLayoutChatResponseContent { + readonly kind = 'horizontal'; + protected _content: ChatResponseContent[]; + + constructor(content: ChatResponseContent[] = []) { + this._content = content; + } + + get content(): ChatResponseContent[] { + return this._content; + } + + asString(): string { + return this._content.map(child => child.asString && child.asString()).join(' '); + } + + merge(nextChatResponseContent: ChatResponseContent): boolean { + if (HorizontalLayoutChatResponseContent.is(nextChatResponseContent)) { + this._content.push(...nextChatResponseContent.content); + } else { + this._content.push(nextChatResponseContent); + } + return true; + } +} + +class ChatResponseImpl implements ChatResponse { + protected readonly _onDidChangeEmitter = new Emitter(); + onDidChange: Event = this._onDidChangeEmitter.event; + protected _content: ChatResponseContent[]; + protected _responseRepresentation: string; + + constructor() { + // TODO accept serialized data as a parameter to restore a previously saved ChatResponse + this._content = []; + } + + get content(): ChatResponseContent[] { + return this._content; + } + + addContents(contents: ChatResponseContent[]): void { + contents.forEach(c => this.doAddContent(c)); + this._onDidChangeEmitter.fire(); + } + + addContent(nextContent: ChatResponseContent): void { + // TODO: Support more complex merges affecting different content than the last, e.g. via some kind of ProcessorRegistry + // TODO: Support more of the built-in VS Code behavior, see + // https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts#L188-L244 + this.doAddContent(nextContent); + this._onDidChangeEmitter.fire(); + } + + protected doAddContent(nextContent: ChatResponseContent): void { + if (ToolCallChatResponseContent.is(nextContent) && nextContent.id !== undefined) { + const fittingTool = this._content.find(c => ToolCallChatResponseContent.is(c) && c.id === nextContent.id); + if (fittingTool !== undefined) { + fittingTool.merge?.(nextContent); + } else { + this._content.push(nextContent); + } + } else { + const lastElement = this._content.length > 0 + ? this._content[this._content.length - 1] + : undefined; + if (lastElement?.kind === nextContent.kind && ChatResponseContent.hasMerge(lastElement)) { + const mergeSuccess = lastElement.merge(nextContent); + if (!mergeSuccess) { + this._content.push(nextContent); + } + } else { + this._content.push(nextContent); + } + } + this._updateResponseRepresentation(); + } + + protected _updateResponseRepresentation(): void { + this._responseRepresentation = this._content + .map(responseContent => { + if (ChatResponseContent.hasAsString(responseContent)) { + return responseContent.asString(); + } + if (TextChatResponseContent.is(responseContent)) { + return responseContent.content; + } + console.warn( + 'Was not able to map responseContent to a string', + responseContent + ); + return undefined; + }) + .filter(text => text !== undefined) + .join('\n\n'); + } + + asString(): string { + return this._responseRepresentation; + } +} + +class ChatResponseModelImpl implements ChatResponseModel { + protected readonly _onDidChangeEmitter = new Emitter(); + onDidChange: Event = this._onDidChangeEmitter.event; + + protected _id: string; + protected _requestId: string; + protected _progressMessages: ChatProgressMessage[]; + protected _response: ChatResponseImpl; + protected _isComplete: boolean; + protected _isCanceled: boolean; + protected _agentId?: string; + protected _isError: boolean; + protected _errorObject: Error | undefined; + + constructor(requestId: string, agentId?: string) { + // TODO accept serialized data as a parameter to restore a previously saved ChatResponseModel + this._requestId = requestId; + this._id = generateUuid(); + this._progressMessages = []; + const response = new ChatResponseImpl(); + response.onDidChange(() => this._onDidChangeEmitter.fire()); + this._response = response; + this._isComplete = false; + this._isCanceled = false; + this._agentId = agentId; + } + + get id(): string { + return this._id; + } + + get requestId(): string { + return this._requestId; + } + + get progressMessages(): ChatProgressMessage[] { + return this._progressMessages; + } + + addProgressMessage(message: { content: string } & Partial>): ChatProgressMessage { + const id = message.id ?? generateUuid(); + const existingMessage = this.getProgressMessage(id); + if (existingMessage) { + this.updateProgressMessage({ id, ...message }); + return existingMessage; + } + const newMessage: ChatProgressMessage = { + kind: 'progressMessage', + id, + status: message.status ?? 'inProgress', + ...message, + }; + this._progressMessages.push(newMessage); + this._onDidChangeEmitter.fire(); + return newMessage; + } + + getProgressMessage(id: string): ChatProgressMessage | undefined { + return this._progressMessages.find(message => message.id === id); + } + + updateProgressMessage(message: { id: string } & Partial>): void { + const progressMessage = this.getProgressMessage(message.id); + if (progressMessage) { + Object.assign(progressMessage, message); + this._onDidChangeEmitter.fire(); + } + } + + get response(): ChatResponseImpl { + return this._response; + } + + get isComplete(): boolean { + return this._isComplete; + } + + get isCanceled(): boolean { + return this._isCanceled; + } + + get agentId(): string | undefined { + return this._agentId; + } + + overrideAgentId(agentId: string): void { + this._agentId = agentId; + } + + complete(): void { + this._isComplete = true; + this._onDidChangeEmitter.fire(); + } + + cancel(): void { + this._isComplete = true; + this._isCanceled = true; + this._onDidChangeEmitter.fire(); + } + error(error: Error): void { + this._isComplete = true; + this._isCanceled = false; + this._isError = true; + this._errorObject = error; + this._onDidChangeEmitter.fire(); + } + get errorObject(): Error | undefined { + return this._errorObject; + } + get isError(): boolean { + return this._isError; + } +} + +export class ErrorChatResponseModelImpl extends ChatResponseModelImpl { + constructor(requestId: string, error: Error, agentId?: string) { + super(requestId, agentId); + this.error(error); + } +} diff --git a/packages/ai-chat/src/common/chat-request-parser.spec.ts b/packages/ai-chat/src/common/chat-request-parser.spec.ts new file mode 100644 index 0000000000000..711c85d36bf68 --- /dev/null +++ b/packages/ai-chat/src/common/chat-request-parser.spec.ts @@ -0,0 +1,120 @@ +// ***************************************************************************** +// 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 * as sinon from 'sinon'; +import { ChatAgentServiceImpl } from './chat-agent-service'; +import { ChatRequestParserImpl } from './chat-request-parser'; +import { ChatAgentLocation } from './chat-agents'; +import { ChatRequest } from './chat-model'; +import { expect } from 'chai'; +import { DefaultAIVariableService, ToolInvocationRegistry, ToolInvocationRegistryImpl } from '@theia/ai-core'; + +describe('ChatRequestParserImpl', () => { + const chatAgentService = sinon.createStubInstance(ChatAgentServiceImpl); + const variableService = sinon.createStubInstance(DefaultAIVariableService); + const toolInvocationRegistry: ToolInvocationRegistry = sinon.createStubInstance(ToolInvocationRegistryImpl); + const parser = new ChatRequestParserImpl(chatAgentService, variableService, toolInvocationRegistry); + + it('parses simple text', () => { + const req: ChatRequest = { + text: 'What is the best pizza topping?' + }; + const result = parser.parseChatRequest(req, ChatAgentLocation.Panel); + expect(result.parts).to.deep.contain({ + text: 'What is the best pizza topping?', + range: { start: 0, endExclusive: 31 } + }); + }); + + it('parses text with variable name', () => { + const req: ChatRequest = { + text: 'What is the #best pizza topping?' + }; + const result = parser.parseChatRequest(req, ChatAgentLocation.Panel); + expect(result).to.deep.contain({ + parts: [{ + text: 'What is the ', + range: { start: 0, endExclusive: 12 } + }, { + variableName: 'best', + variableArg: undefined, + range: { start: 12, endExclusive: 17 } + }, { + text: ' pizza topping?', + range: { start: 17, endExclusive: 32 } + }] + }); + }); + + it('parses text with variable name with argument', () => { + const req: ChatRequest = { + text: 'What is the #best:by-poll pizza topping?' + }; + const result = parser.parseChatRequest(req, ChatAgentLocation.Panel); + expect(result).to.deep.contain({ + parts: [{ + text: 'What is the ', + range: { start: 0, endExclusive: 12 } + }, { + variableName: 'best', + variableArg: 'by-poll', + range: { start: 12, endExclusive: 25 } + }, { + text: ' pizza topping?', + range: { start: 25, endExclusive: 40 } + }] + }); + }); + + it('parses text with variable name with numeric argument', () => { + const req: ChatRequest = { + text: '#size-class:2' + }; + const result = parser.parseChatRequest(req, ChatAgentLocation.Panel); + expect(result.parts[0]).to.contain( + { + variableName: 'size-class', + variableArg: '2' + } + ); + }); + + it('parses text with variable name with POSIX path argument', () => { + const req: ChatRequest = { + text: '#file:/path/to/file.ext' + }; + const result = parser.parseChatRequest(req, ChatAgentLocation.Panel); + expect(result.parts[0]).to.contain( + { + variableName: 'file', + variableArg: '/path/to/file.ext' + } + ); + }); + + it('parses text with variable name with Win32 path argument', () => { + const req: ChatRequest = { + text: '#file:c:\\path\\to\\file.ext' + }; + const result = parser.parseChatRequest(req, ChatAgentLocation.Panel); + expect(result.parts[0]).to.contain( + { + variableName: 'file', + variableArg: 'c:\\path\\to\\file.ext' + } + ); + }); +}); diff --git a/packages/ai-chat/src/common/chat-request-parser.ts b/packages/ai-chat/src/common/chat-request-parser.ts new file mode 100644 index 0000000000000..8c21aaa4304c7 --- /dev/null +++ b/packages/ai-chat/src/common/chat-request-parser.ts @@ -0,0 +1,220 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatRequestParser.ts + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ChatAgentService } from './chat-agent-service'; +import { ChatAgentLocation } from './chat-agents'; +import { ChatRequest } from './chat-model'; +import { + chatAgentLeader, + chatFunctionLeader, + ParsedChatRequestAgentPart, + ParsedChatRequestFunctionPart, + ParsedChatRequestTextPart, + ParsedChatRequestVariablePart, + chatVariableLeader, + OffsetRange, + ParsedChatRequest, + ParsedChatRequestPart, +} from './parsed-chat-request'; +import { AIVariable, AIVariableService, ToolInvocationRegistry, ToolRequest } from '@theia/ai-core'; + +const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent +const functionReg = /^~([\w_\-\.]+)(?=(\s|$|\b))/i; // A ~ tool function +const variableReg = /^#([\w_\-]+)(?::([\w_\-_\/\\.:]+))?(?=(\s|$|\b))/i; // A #-variable with an optional : arg (#file:workspace/path/name.ext) + +export const ChatRequestParser = Symbol('ChatRequestParser'); +export interface ChatRequestParser { + parseChatRequest(request: ChatRequest, location: ChatAgentLocation): ParsedChatRequest; +} + +function offsetRange(start: number, endExclusive: number): OffsetRange { + if (start > endExclusive) { + throw new Error(`Invalid range: start=${start} endExclusive=${endExclusive}`); + } + return { start, endExclusive }; +} +@injectable() +export class ChatRequestParserImpl { + constructor( + @inject(ChatAgentService) private readonly agentService: ChatAgentService, + @inject(AIVariableService) private readonly variableService: AIVariableService, + @inject(ToolInvocationRegistry) private readonly toolInvocationRegistry: ToolInvocationRegistry + ) { } + + parseChatRequest(request: ChatRequest, location: ChatAgentLocation): ParsedChatRequest { + const parts: ParsedChatRequestPart[] = []; + const variables = new Map(); + const toolRequests = new Map(); + const message = request.text; + for (let i = 0; i < message.length; i++) { + const previousChar = message.charAt(i - 1); + const char = message.charAt(i); + let newPart: ParsedChatRequestPart | undefined; + + if (previousChar.match(/\s/) || i === 0) { + if (char === chatFunctionLeader) { + const functionPart = this.tryParseFunction( + message.slice(i), + i + ); + newPart = functionPart; + if (functionPart) { + toolRequests.set(functionPart.toolRequest.id, functionPart.toolRequest); + } + } else if (char === chatVariableLeader) { + const variablePart = this.tryToParseVariable( + message.slice(i), + i, + parts + ); + newPart = variablePart; + if (variablePart) { + const variable = this.variableService.getVariable(variablePart.variableName); + if (variable) { + variables.set(variable.name, variable); + } + } + } else if (char === chatAgentLeader) { + newPart = this.tryToParseAgent( + message.slice(i), + i, + parts, + location + ); + } + } + + if (newPart) { + if (i !== 0) { + // Insert a part for all the text we passed over, then insert the new parsed part + const previousPart = parts.at(-1); + const previousPartEnd = + previousPart?.range.endExclusive ?? 0; + parts.push( + new ParsedChatRequestTextPart( + offsetRange(previousPartEnd, i), + message.slice(previousPartEnd, i) + ) + ); + } + + parts.push(newPart); + } + } + + const lastPart = parts.at(-1); + const lastPartEnd = lastPart?.range.endExclusive ?? 0; + if (lastPartEnd < message.length) { + parts.push( + new ParsedChatRequestTextPart( + offsetRange(lastPartEnd, message.length), + message.slice(lastPartEnd, message.length) + ) + ); + } + + return { request, parts, toolRequests, variables }; + } + + private tryToParseAgent( + message: string, + offset: number, + parts: ReadonlyArray, + location: ChatAgentLocation + ): ParsedChatRequestAgentPart | ParsedChatRequestVariablePart | undefined { + const nextAgentMatch = message.match(agentReg); + if (!nextAgentMatch) { + return; + } + + const [full, name] = nextAgentMatch; + const agentRange = offsetRange(offset, offset + full.length); + + let agents = this.agentService.getAgents().filter(a => a.name === name); + if (!agents.length) { + const fqAgent = this.agentService.getAgent(name); + if (fqAgent) { + agents = [fqAgent]; + } + } + + // If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the + // context and we use that one. Otherwise just pick the first. + const agent = agents[0]; + if (!agent || !agent.locations.includes(location)) { + return; + } + + if (parts.some(p => p instanceof ParsedChatRequestAgentPart)) { + // Only one agent allowed + return; + } + + // The agent must come first + if ( + parts.some( + p => + (p instanceof ParsedChatRequestTextPart && + p.text.trim() !== '') || + !(p instanceof ParsedChatRequestAgentPart) + ) + ) { + return; + } + + return new ParsedChatRequestAgentPart(agentRange, agent.id, agent.name); + } + + private tryToParseVariable( + message: string, + offset: number, + _parts: ReadonlyArray + ): ParsedChatRequestVariablePart | undefined { + const nextVariableMatch = message.match(variableReg); + if (!nextVariableMatch) { + return; + } + + const [full, name] = nextVariableMatch; + const variableArg = nextVariableMatch[2]; + const varRange = offsetRange(offset, offset + full.length); + + return new ParsedChatRequestVariablePart(varRange, name, variableArg); + } + + private tryParseFunction(message: string, offset: number): ParsedChatRequestFunctionPart | undefined { + const nextFunctionMatch = message.match(functionReg); + if (!nextFunctionMatch) { + return; + } + + const [full, id] = nextFunctionMatch; + + const maybeToolRequest = this.toolInvocationRegistry.getFunction(id); + if (!maybeToolRequest) { + return; + } + + const functionRange = offsetRange(offset, offset + full.length); + return new ParsedChatRequestFunctionPart(functionRange, maybeToolRequest); + } +} diff --git a/packages/ai-chat/src/common/chat-service.ts b/packages/ai-chat/src/common/chat-service.ts new file mode 100644 index 0000000000000..3aeb8dcd612a0 --- /dev/null +++ b/packages/ai-chat/src/common/chat-service.ts @@ -0,0 +1,236 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatService.ts + +import { inject, injectable, optional } from '@theia/core/shared/inversify'; +import { + ChatModel, + ChatModelImpl, + ChatRequest, + ChatRequestModel, + ChatResponseModel, + ErrorChatResponseModelImpl, +} from './chat-model'; +import { ChatAgentService } from './chat-agent-service'; +import { Emitter, ILogger, generateUuid } from '@theia/core'; +import { ChatRequestParser } from './chat-request-parser'; +import { ChatAgent, ChatAgentLocation } from './chat-agents'; +import { ParsedChatRequestAgentPart, ParsedChatRequestVariablePart, ParsedChatRequest } from './parsed-chat-request'; +import { AIVariableService } from '@theia/ai-core'; +import { Event } from '@theia/core/shared/vscode-languageserver-protocol'; + +export interface ChatRequestInvocation { + /** + * Promise which completes once the request preprocessing is complete. + */ + requestCompleted: Promise; + /** + * Promise which completes once a response is expected to arrive. + */ + responseCreated: Promise; + /** + * Promise which completes once the response is complete. + */ + responseCompleted: Promise; +} + +export interface ChatSession { + id: string; + title?: string; + model: ChatModel; + isActive: boolean; +} + +export interface ActiveSessionChangedEvent { + sessionId: string | undefined; + focus?: boolean; +} + +export interface SessionOptions { + focus?: boolean; +} + +export const DefaultChatAgentId = Symbol('DefaultChatAgentId'); +export interface DefaultChatAgentId { + id: string; +} + +export const ChatService = Symbol('ChatService'); +export interface ChatService { + onActiveSessionChanged: Event + + getSession(id: string): ChatSession | undefined; + getSessions(): ChatSession[]; + createSession(location?: ChatAgentLocation, options?: SessionOptions): ChatSession; + deleteSession(sessionId: string): void; + setActiveSession(sessionId: string, options?: SessionOptions): void; + + sendRequest( + sessionId: string, + request: ChatRequest + ): Promise; +} + +interface ChatSessionInternal extends ChatSession { + model: ChatModelImpl; +} + +@injectable() +export class ChatServiceImpl implements ChatService { + protected readonly onActiveSessionChangedEmitter = new Emitter(); + onActiveSessionChanged = this.onActiveSessionChangedEmitter.event; + + @inject(ChatAgentService) + protected chatAgentService: ChatAgentService; + + @inject(DefaultChatAgentId) @optional() + protected defaultChatAgentId: DefaultChatAgentId | undefined; + + @inject(ChatRequestParser) + protected chatRequestParser: ChatRequestParser; + + @inject(AIVariableService) + protected variableService: AIVariableService; + + @inject(ILogger) + protected logger: ILogger; + + protected _sessions: ChatSessionInternal[] = []; + + getSessions(): ChatSessionInternal[] { + return [...this._sessions]; + } + + getSession(id: string): ChatSessionInternal | undefined { + return this._sessions.find(session => session.id === id); + } + + createSession(location = ChatAgentLocation.Panel, options?: SessionOptions): ChatSession { + const model = new ChatModelImpl(location); + const session: ChatSessionInternal = { + id: model.id, + model, + isActive: true + }; + this._sessions.push(session); + this.setActiveSession(session.id, options); + return session; + } + + deleteSession(sessionId: string): void { + // If the removed session is the active one, set the newest one as active + if (this.getSession(sessionId)?.isActive) { + this.setActiveSession(this._sessions[this._sessions.length - 1]?.id); + } + this._sessions = this._sessions.filter(item => item.id !== sessionId); + } + + setActiveSession(sessionId: string | undefined, options?: SessionOptions): void { + this._sessions.forEach(session => { + session.isActive = session.id === sessionId; + }); + this.onActiveSessionChangedEmitter.fire({ sessionId: sessionId, ...options }); + } + + async sendRequest( + sessionId: string, + request: ChatRequest + ): Promise { + const session = this.getSession(sessionId); + if (!session) { + return undefined; + } + session.title = request.text; + + const parsedRequest = this.chatRequestParser.parseChatRequest(request, session.model.location); + + const agent = this.getAgent(parsedRequest); + if (agent === undefined) { + const error = 'No ChatAgents available to handle request!'; + this.logger.error(error); + const chatResponseModel = new ErrorChatResponseModelImpl(generateUuid(), new Error(error)); + return { + requestCompleted: Promise.reject(error), + responseCreated: Promise.reject(error), + responseCompleted: Promise.resolve(chatResponseModel), + }; + } + const requestModel = session.model.addRequest(parsedRequest, agent?.id); + + for (const part of parsedRequest.parts) { + if (part instanceof ParsedChatRequestVariablePart) { + const resolvedVariable = await this.variableService.resolveVariable( + { variable: part.variableName, arg: part.variableArg }, + { request, model: session } + ); + if (resolvedVariable) { + part.resolution = resolvedVariable; + } else { + this.logger.warn(`Failed to resolve variable ${part.variableName} for ${session.model.location}`); + } + } + } + + let resolveResponseCreated: (responseModel: ChatResponseModel) => void; + let resolveResponseCompleted: (responseModel: ChatResponseModel) => void; + const invocation: ChatRequestInvocation = { + requestCompleted: Promise.resolve(requestModel), + responseCreated: new Promise(resolve => { + resolveResponseCreated = resolve; + }), + responseCompleted: new Promise(resolve => { + resolveResponseCompleted = resolve; + }), + }; + + resolveResponseCreated!(requestModel.response); + requestModel.response.onDidChange(() => { + if (requestModel.response.isComplete) { + resolveResponseCompleted!(requestModel.response); + } + if (requestModel.response.isError) { + resolveResponseCompleted!(requestModel.response); + } + }); + + if (agent) { + agent.invoke(requestModel).catch(error => requestModel.response.error(error)); + } else { + this.logger.error('No ChatAgents available to handle request!', requestModel); + } + + return invocation; + } + + protected getAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined { + const agentPart = this.getMentionedAgent(parsedRequest); + if (agentPart) { + return this.chatAgentService.getAgent(agentPart.agentId); + } + if (this.defaultChatAgentId) { + return this.chatAgentService.getAgent(this.defaultChatAgentId.id); + } + return this.chatAgentService.getAgents()[0] ?? undefined; + } + + protected getMentionedAgent(parsedRequest: ParsedChatRequest): ParsedChatRequestAgentPart | undefined { + return parsedRequest.parts.find(p => p instanceof ParsedChatRequestAgentPart) as ParsedChatRequestAgentPart | undefined; + } +} diff --git a/packages/ai-chat/src/common/command-chat-agents.ts b/packages/ai-chat/src/common/command-chat-agents.ts new file mode 100644 index 0000000000000..7ab6608f049de --- /dev/null +++ b/packages/ai-chat/src/common/command-chat-agents.ts @@ -0,0 +1,352 @@ +// ***************************************************************************** +// 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 { AbstractTextToModelParsingChatAgent, ChatAgent, SystemMessageDescription } from './chat-agents'; +import { + PromptTemplate, + AgentSpecificVariables +} from '@theia/ai-core'; +import { + ChatRequestModelImpl, + ChatResponseContent, + CommandChatResponseContentImpl, + CustomCallback, + HorizontalLayoutChatResponseContentImpl, + MarkdownChatResponseContentImpl, +} from './chat-model'; +import { + CommandRegistry, + MessageService, + generateUuid, +} from '@theia/core'; + +export const commandTemplate: PromptTemplate = { + id: 'command-system', + template: `# System Prompt + +You are a service that helps users find commands to execute in an IDE. +You reply with stringified JSON Objects that tell the user which command to execute and its arguments, if any. + +# Examples + +The examples start with a short explanation of the return object. +The response can be found within the markdown \`\`\`json and \`\`\` markers. +Please include these markers in the reply. + +Never under any circumstances may you reply with just the command-id! + +## Example 1 + +This reply is to tell the user to execute the \`theia-ai-prompt-template:show-prompts-command\` command that is available in the Theia command registry. + +\`\`\`json +{ + "type": "theia-command", + "commandId": "theia-ai-prompt-template:show-prompts-command" +} +\`\`\` + +## Example 2 + +This reply is to tell the user to execute the \`theia-ai-prompt-template:show-prompts-command\` command that is available in the theia command registry, +when the user want to pass arguments to the command. + +\`\`\`json +{ + "type": "theia-command", + "commandId": "theia-ai-prompt-template:show-prompts-command", + "arguments": ["foo"] +} +\`\`\` + +## Example 3 + +This reply is for custom commands that are not registered in the Theia command registry. +These commands always have the command id \`ai-chat.command-chat-response.generic\`. +The arguments are an array and may differ, depending on the user's instructions. + +\`\`\`json +{ + "type": "custom-handler", + "commandId": "ai-chat.command-chat-response.generic", + "arguments": ["foo", "bar"] +} +\`\`\` + +## Example 4 + +This reply of type no-command is for cases where you can't find a proper command. +You may use the message to explain the situation to the user. + +\`\`\`json +{ + "type": "no-command", + "message": "a message explaining what is wrong" +} +\`\`\` + +# Rules + +## Theia Commands + +If a user asks for a Theia command, or the context implies it is about a command in Theia, return a response with \`"type": "theia-command"\`. +You need to exchange the "commandId". +The available command ids in Theia are in the list below. The list of commands is formatted like this: + +command-id1: Label1 +command-id2: Label2 +command-id3: +command-id4: Label4 + +The Labels may be empty, but there is always a command-id. + +Suggest a command that probably fits the user's message based on the label and the command ids you know. +If you have multiple commands that fit, return the one that fits best. We only want a single command in the reply. +If the user says that the last command was not right, try to return the next best fit based on the conversation history with the user. + +If there are no more command ids that seem to fit, return a response of \`"type": "no-command"\` explaining the situation. + +Here are the known Theia commands: + +Begin List: +{{command-ids}} +End List + +You may only use commands from this list when responding with \`"type": "theia-command"\`. +Do not come up with command ids that are not in this list. +If you need to do this, use the \`"type": "no-command"\`. instead + +## Custom Handlers + +If the user asks for a command that is not a Theia command, return a response with \`"type": "custom-handler"\`. + +## Other Cases + +In all other cases, return a reply of \`"type": "no-command"\`. + +# Examples of Invalid Responses + +## Invalid Response Example 1 + +This example is invalid because it returns text and two commands. +Only one command should be replied, and it must be parseable JSON. + +### The Example + +Yes, there are a few more theme-related commands. Here is another one: + +\`\`\`json +{ + "type": "theia-command", + "commandId": "workbench.action.selectIconTheme" +} +\`\`\` + +And another one: + +\`\`\`json +{ + "type": "theia-command", + "commandId": "core.close.right.tabs" +} +\`\`\` + +## Invalid Response Example 2 + +The following example is invalid because it only returns the command id and is not parseable JSON: + +### The Example + +workbench.action.selectIconTheme + +## Invalid Response Example 3 + +The following example is invalid because it returns a message with the command id. We need JSON objects based on the above rules. +Do not respond like this in any case! We need a command of \`"type": "theia-command"\`. + +The expected response would be: +\`\`\`json +{ + "type": "theia-command", + "commandId": "core.close.right.tabs" +} +\`\`\` + +### The Example + +I found this command that might help you: core.close.right.tabs + +## Invalid Response Example 4 + +The following example is invalid because it has an explanation string before the JSON. +We only want the JSON! + +### The Example + +You can toggle high contrast mode with this command: + +\`\`\`json +{ + "type": "theia-command", + "commandId": "editor.action.toggleHighContrast" +} +\`\`\` + +## Invalid Response Example 5 + +The following example is invalid because it explains that no command was found. +We want a response of \`"type": "no-command"\` and have the message there. + +### The Example + +There is no specific command available to "open the windows" in the provided Theia command list. + +## Invalid Response Example 6 + +In this example we were using the following theia id command list: + +Begin List: +container--theia-open-editors-widget: Hello +foo:toggle-visibility-explorer-view-container--files: Label 1 +foo:toggle-visibility-explorer-view-container--plugin-view: Label 2 +End List + +The problem is that workbench.action.toggleHighContrast is not in this list. +theia-command types may only use commandIds from this list. +This should have been of \`"type": "no-command"\`. + +### The Example + +\`\`\`json +{ + "type": "theia-command", + "commandId": "workbench.action.toggleHighContrast" +} +\`\`\` + +`}; + +interface ParsedCommand { + type: 'theia-command' | 'custom-handler' | 'no-command' + commandId: string; + arguments?: string[]; + message?: string; +} + +@injectable() +export class CommandChatAgent extends AbstractTextToModelParsingChatAgent implements ChatAgent { + @inject(CommandRegistry) + protected commandRegistry: CommandRegistry; + @inject(MessageService) + protected messageService: MessageService; + readonly name: string; + readonly description: string; + readonly variables: string[]; + readonly promptTemplates: PromptTemplate[]; + readonly functions: string[]; + readonly agentSpecificVariables: AgentSpecificVariables[]; + + constructor( + ) { + super('Command', [{ + purpose: 'command', + identifier: 'openai/gpt-4o', + }], 'command'); + this.name = 'Command'; + this.description = 'This agent is aware of all commands that the user can execute within the Theia IDE, the tool that the user is currently working with. \ + Based on the user request, it can find the right command and then let the user execute it.'; + this.variables = []; + this.promptTemplates = [commandTemplate]; + this.functions = []; + this.agentSpecificVariables = [{ + name: 'command-ids', + description: 'The list of available commands in Theia.', + usedInPrompt: true + }]; + } + + protected async getSystemMessageDescription(): Promise { + const knownCommands: string[] = []; + for (const command of this.commandRegistry.getAllCommands()) { + knownCommands.push(`${command.id}: ${command.label}`); + } + const systemPrompt = await this.promptService.getPrompt(commandTemplate.id, { + 'command-ids': knownCommands.join('\n') + }); + if (systemPrompt === undefined) { + throw new Error('Couldn\'t get system prompt '); + } + return SystemMessageDescription.fromResolvedPromptTemplate(systemPrompt); + } + + /** + * @param text the text received from the language model + * @returns the parsed command if the text contained a valid command. + * If there was no json in the text, return a no-command response. + */ + protected async parseTextResponse(text: string): Promise { + const jsonMatch = text.match(/(\{[\s\S]*\})/); + const jsonString = jsonMatch ? jsonMatch[1] : `{ + "type": "no-command", + "message": "Please try again." +}`; + const parsedCommand = JSON.parse(jsonString) as ParsedCommand; + return parsedCommand; + } + + protected createResponseContent(parsedCommand: ParsedCommand, request: ChatRequestModelImpl): ChatResponseContent { + if (parsedCommand.type === 'theia-command') { + const theiaCommand = this.commandRegistry.getCommand(parsedCommand.commandId); + if (theiaCommand === undefined) { + console.error(`No Theia Command with id ${parsedCommand.commandId}`); + request.response.cancel(); + } + const args = parsedCommand.arguments !== undefined && + parsedCommand.arguments.length > 0 + ? parsedCommand.arguments + : undefined; + + return new HorizontalLayoutChatResponseContentImpl([ + new MarkdownChatResponseContentImpl( + 'I found this command that might help you:' + ), + new CommandChatResponseContentImpl(theiaCommand, undefined, args), + ]); + } else if (parsedCommand.type === 'custom-handler') { + const id = `ai-command-${generateUuid()}`; + const commandArgs = parsedCommand.arguments !== undefined && parsedCommand.arguments.length > 0 ? parsedCommand.arguments : []; + const args = [id, ...commandArgs]; + const customCallback: CustomCallback = { + label: 'AI command', + callback: () => this.commandCallback(...args), + }; + return new HorizontalLayoutChatResponseContentImpl([ + new MarkdownChatResponseContentImpl( + 'Try executing this:' + ), + new CommandChatResponseContentImpl(undefined, customCallback, args), + ]); + } else { + return new MarkdownChatResponseContentImpl(parsedCommand.message ?? 'Sorry, I can\'t find such a command'); + } + } + + protected async commandCallback(...commandArgs: unknown[]): Promise { + this.messageService.info(`Executing callback with args ${commandArgs.join(', ')}. The first arg is the command id registered for the dynamically registered command. + The other args are the actual args for the handler.`, 'Got it'); + } +} diff --git a/packages/ai-chat/src/common/custom-chat-agent.ts b/packages/ai-chat/src/common/custom-chat-agent.ts new file mode 100644 index 0000000000000..52743d654dba7 --- /dev/null +++ b/packages/ai-chat/src/common/custom-chat-agent.ts @@ -0,0 +1,44 @@ +// ***************************************************************************** +// 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 { AgentSpecificVariables, PromptTemplate } from '@theia/ai-core'; +import { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } from './chat-agents'; +import { injectable } from '@theia/core/shared/inversify'; + +@injectable() +export class CustomChatAgent + extends AbstractStreamParsingChatAgent + implements ChatAgent { + name: string; + description: string; + readonly variables: string[] = []; + readonly functions: string[] = []; + readonly promptTemplates: PromptTemplate[] = []; + readonly agentSpecificVariables: AgentSpecificVariables[] = []; + + constructor( + ) { + super('CustomChatAgent', [{ purpose: 'chat' }], 'chat'); + } + protected override async getSystemMessageDescription(): Promise { + const resolvedPrompt = await this.promptService.getPrompt(`${this.name}_prompt`); + return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined; + } + + set prompt(prompt: string) { + this.promptTemplates.push({ id: `${this.name}_prompt`, template: prompt }); + } +} diff --git a/packages/ai-chat/src/common/index.ts b/packages/ai-chat/src/common/index.ts new file mode 100644 index 0000000000000..cf160ddcadf10 --- /dev/null +++ b/packages/ai-chat/src/common/index.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 +// ***************************************************************************** +export * from './chat-agents'; +export * from './chat-agent-service'; +export * from './chat-model'; +export * from './chat-request-parser'; +export * from './chat-service'; +export * from './command-chat-agents'; +export * from './custom-chat-agent'; +export * from './parsed-chat-request'; +export * from './orchestrator-chat-agent'; +export * from './universal-chat-agent'; diff --git a/packages/ai-chat/src/common/orchestrator-chat-agent.ts b/packages/ai-chat/src/common/orchestrator-chat-agent.ts new file mode 100644 index 0000000000000..c74195d9ca444 --- /dev/null +++ b/packages/ai-chat/src/common/orchestrator-chat-agent.ts @@ -0,0 +1,176 @@ +// ***************************************************************************** +// 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 { AgentSpecificVariables, getJsonOfText, getTextOfResponse, LanguageModelResponse } from '@theia/ai-core'; +import { + PromptTemplate +} from '@theia/ai-core/lib/common'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ChatAgentService } from './chat-agent-service'; +import { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } from './chat-agents'; +import { ChatRequestModelImpl, InformationalChatResponseContentImpl } from './chat-model'; +import { generateUuid } from '@theia/core'; + +export const orchestratorTemplate: PromptTemplate = { + id: 'orchestrator-system', + template: `# Instructions + +Your task is to identify which Chat Agent(s) should best reply a given user's message. +You consider all messages of the conversation to ensure consistency and avoid agent switches without a clear context change. +You should select the best Chat Agent based on the name and description of the agents, matching them to the user message. + +## Constraints + +Your response must be a JSON array containing the id(s) of the selected Chat Agent(s). + +* Do not use ids that are not provided in the list below. +* Do not include any additional information, explanations, or questions for the user. +* If there is no suitable choice, pick \`Universal\`. +* If there are multiple good choices, return all of them. + +Unless there is a more specific agent available, select \`Universal\`, especially for general programming-related questions. +You must only use the \`id\` attribute of the agent, never the name. + +### Example Results + +\`\`\`json +["Universal"] +\`\`\` + +\`\`\`json +["AnotherChatAgent", "Universal"] +\`\`\` + +## List of Currently Available Chat Agents + +{{chatAgents}} +`}; + +export const OrchestratorChatAgentId = 'Orchestrator'; +const OrchestatorRequestIdKey = 'orchestatorRequestIdKey'; + +@injectable() +export class OrchestratorChatAgent extends AbstractStreamParsingChatAgent implements ChatAgent { + name: string; + description: string; + readonly variables: string[]; + promptTemplates: PromptTemplate[]; + fallBackChatAgentId: string; + readonly functions: string[] = []; + readonly agentSpecificVariables: AgentSpecificVariables[] = []; + + constructor() { + super(OrchestratorChatAgentId, [{ + purpose: 'agent-selection', + identifier: 'openai/gpt-4o', + }], 'agent-selection', 'codicon codicon-symbol-boolean', undefined, undefined, false); + this.name = OrchestratorChatAgentId; + this.description = 'This agent analyzes the user request against the description of all available chat agents and selects the best fitting agent to answer the request \ + (by using AI).The user\'s request will be directly delegated to the selected agent without further confirmation.'; + this.variables = ['chatAgents']; + this.promptTemplates = [orchestratorTemplate]; + this.fallBackChatAgentId = 'Universal'; + this.functions = []; + this.agentSpecificVariables = []; + } + + @inject(ChatAgentService) + protected chatAgentService: ChatAgentService; + + override async invoke(request: ChatRequestModelImpl): Promise { + request.response.addProgressMessage({ content: 'Determining the most appropriate agent', status: 'inProgress' }); + // We generate a dedicated ID for recording the orchestrator request/response, as we will forward the original request to another agent + const orchestartorRequestId = generateUuid(); + request.addData(OrchestatorRequestIdKey, orchestartorRequestId); + const userPrompt = request.request.text; + this.recordingService.recordRequest({ + agentId: this.id, + sessionId: request.session.id, + timestamp: Date.now(), + requestId: orchestartorRequestId, + request: userPrompt, + }); + return super.invoke(request); + } + + protected async getSystemMessageDescription(): Promise { + const resolvedPrompt = await this.promptService.getPrompt(orchestratorTemplate.id); + return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined; + } + + protected override async addContentsToResponse(response: LanguageModelResponse, request: ChatRequestModelImpl): Promise { + let agentIds: string[] = []; + const responseText = await getTextOfResponse(response); + // We use the previously generated, dedicated ID to log the orchestrator response before we forward the original request + const orchestratorRequestId = request.getDataByKey(OrchestatorRequestIdKey); + if (typeof orchestratorRequestId === 'string') { + this.recordingService.recordResponse({ + agentId: this.id, + sessionId: request.session.id, + timestamp: Date.now(), + requestId: orchestratorRequestId, + response: responseText, + }); + } + try { + const jsonResponse = await getJsonOfText(responseText); + if (Array.isArray(jsonResponse)) { + agentIds = jsonResponse.filter((id: string) => id !== this.id); + } + } catch (error: unknown) { + // The llm sometimes does not return a parseable result + this.logger.error('Failed to parse JSON response', error); + } + + if (agentIds.length < 1) { + this.logger.error('No agent was selected, delegating to fallback chat agent'); + request.response.progressMessages.forEach(progressMessage => + request.response.updateProgressMessage({ ...progressMessage, status: 'failed' }) + ); + agentIds = [this.fallBackChatAgentId]; + } + + // check if selected (or fallback) agent exists + if (!this.chatAgentService.getAgent(agentIds[0])) { + this.logger.error(`Chat agent ${agentIds[0]} not found. Falling back to first registered agent.`); + const firstRegisteredAgent = this.chatAgentService.getAgents().filter(a => a.id !== this.id)[0]?.id; + if (firstRegisteredAgent) { + agentIds = [firstRegisteredAgent]; + } else { + throw new Error('No chat agent available to handle request. Please check your configuration whether any are enabled.'); + } + } + + // TODO support delegating to more than one agent + const delegatedToAgent = agentIds[0]; + request.response.response.addContent(new InformationalChatResponseContentImpl( + `*Orchestrator*: Delegating to \`@${delegatedToAgent}\` + + --- + + ` + )); + request.response.overrideAgentId(delegatedToAgent); + request.response.progressMessages.forEach(progressMessage => + request.response.updateProgressMessage({ ...progressMessage, status: 'completed' }) + ); + const agent = this.chatAgentService.getAgent(delegatedToAgent); + if (!agent) { + throw new Error(`Chat agent ${delegatedToAgent} not found.`); + } + await agent.invoke(request); + } +} diff --git a/packages/ai-chat/src/common/parse-contents.spec.ts b/packages/ai-chat/src/common/parse-contents.spec.ts new file mode 100644 index 0000000000000..c0a009f8cb814 --- /dev/null +++ b/packages/ai-chat/src/common/parse-contents.spec.ts @@ -0,0 +1,142 @@ +// ***************************************************************************** +// 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 { expect } from 'chai'; +import { ChatResponseContent, CodeChatResponseContentImpl, MarkdownChatResponseContentImpl } from './chat-model'; +import { parseContents } from './parse-contents'; +import { CodeContentMatcher, ResponseContentMatcher } from './response-content-matcher'; + +export class CommandChatResponseContentImpl implements ChatResponseContent { + constructor(public readonly command: string) { } + kind = 'command'; +} + +export const CommandContentMatcher: ResponseContentMatcher = { + start: /^$/m, + end: /^<\/command>$/m, + contentFactory: (content: string) => { + const code = content.replace(/^\n|<\/command>$/g, ''); + return new CommandChatResponseContentImpl(code.trim()); + } +}; + +describe('parseContents', () => { + it('should parse code content', () => { + const text = '```typescript\nconsole.log("Hello World");\n```'; + const result = parseContents(text); + expect(result).to.deep.equal([new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript')]); + }); + + it('should parse markdown content', () => { + const text = 'Hello **World**'; + const result = parseContents(text); + expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('Hello **World**')]); + }); + + it('should parse multiple content blocks', () => { + const text = '```typescript\nconsole.log("Hello World");\n```\nHello **World**'; + const result = parseContents(text); + expect(result).to.deep.equal([ + new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'), + new MarkdownChatResponseContentImpl('\nHello **World**') + ]); + }); + + it('should parse multiple content blocks with different languages', () => { + const text = '```typescript\nconsole.log("Hello World");\n```\n```python\nprint("Hello World")\n```'; + const result = parseContents(text); + expect(result).to.deep.equal([ + new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'), + new CodeChatResponseContentImpl('print("Hello World")', 'python') + ]); + }); + + it('should parse multiple content blocks with different languages and markdown', () => { + const text = '```typescript\nconsole.log("Hello World");\n```\nHello **World**\n```python\nprint("Hello World")\n```'; + const result = parseContents(text); + expect(result).to.deep.equal([ + new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'), + new MarkdownChatResponseContentImpl('\nHello **World**\n'), + new CodeChatResponseContentImpl('print("Hello World")', 'python') + ]); + }); + + it('should parse content blocks with empty content', () => { + const text = '```typescript\n```\nHello **World**\n```python\nprint("Hello World")\n```'; + const result = parseContents(text); + expect(result).to.deep.equal([ + new CodeChatResponseContentImpl('', 'typescript'), + new MarkdownChatResponseContentImpl('\nHello **World**\n'), + new CodeChatResponseContentImpl('print("Hello World")', 'python') + ]); + }); + + it('should parse content with markdown, code, and markdown', () => { + const text = 'Hello **World**\n```typescript\nconsole.log("Hello World");\n```\nGoodbye **World**'; + const result = parseContents(text); + expect(result).to.deep.equal([ + new MarkdownChatResponseContentImpl('Hello **World**\n'), + new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'), + new MarkdownChatResponseContentImpl('\nGoodbye **World**') + ]); + }); + + it('should handle text with no special content', () => { + const text = 'Just some plain text.'; + const result = parseContents(text); + expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('Just some plain text.')]); + }); + + it('should handle text with only start code block', () => { + const text = '```typescript\nconsole.log("Hello World");'; + const result = parseContents(text); + expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('```typescript\nconsole.log("Hello World");')]); + }); + + it('should handle text with only end code block', () => { + const text = 'console.log("Hello World");\n```'; + const result = parseContents(text); + expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('console.log("Hello World");\n```')]); + }); + + it('should handle text with unmatched code block', () => { + const text = '```typescript\nconsole.log("Hello World");\n```\n```python\nprint("Hello World")'; + const result = parseContents(text); + expect(result).to.deep.equal([ + new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'), + new MarkdownChatResponseContentImpl('\n```python\nprint("Hello World")') + ]); + }); + + it('should parse code block without newline after language', () => { + const text = '```typescript console.log("Hello World");```'; + const result = parseContents(text); + expect(result).to.deep.equal([ + new MarkdownChatResponseContentImpl('```typescript console.log("Hello World");```') + ]); + }); + + it('should parse with matches of multiple different matchers and default', () => { + const text = '\nMY_SPECIAL_COMMAND\n\nHello **World**\n```python\nprint("Hello World")\n```\n\nMY_SPECIAL_COMMAND2\n'; + const result = parseContents(text, [CodeContentMatcher, CommandContentMatcher]); + expect(result).to.deep.equal([ + new CommandChatResponseContentImpl('MY_SPECIAL_COMMAND'), + new MarkdownChatResponseContentImpl('\nHello **World**\n'), + new CodeChatResponseContentImpl('print("Hello World")', 'python'), + new CommandChatResponseContentImpl('MY_SPECIAL_COMMAND2'), + ]); + }); +}); diff --git a/packages/ai-chat/src/common/parse-contents.ts b/packages/ai-chat/src/common/parse-contents.ts new file mode 100644 index 0000000000000..16f405495ce20 --- /dev/null +++ b/packages/ai-chat/src/common/parse-contents.ts @@ -0,0 +1,92 @@ +/* + * 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 './chat-model'; +import { CodeContentMatcher, MarkdownContentFactory, ResponseContentFactory, ResponseContentMatcher } from './response-content-matcher'; + +interface Match { + matcher: ResponseContentMatcher; + index: number; + content: string; +} + +export function parseContents( + text: string, + contentMatchers: ResponseContentMatcher[] = [CodeContentMatcher], + defaultContentFactory: ResponseContentFactory = MarkdownContentFactory +): ChatResponseContent[] { + const result: ChatResponseContent[] = []; + + let currentIndex = 0; + while (currentIndex < text.length) { + const remainingText = text.substring(currentIndex); + const match = findFirstMatch(contentMatchers, remainingText); + if (!match) { + // Add the remaining text as default content + if (remainingText.length > 0) { + result.push(defaultContentFactory(remainingText)); + } + break; + } + // We have a match + // 1. Add preceding text as default content + if (match.index > 0) { + const precedingContent = remainingText.substring(0, match.index); + if (precedingContent.trim().length > 0) { + result.push(defaultContentFactory(precedingContent)); + } + } + // 2. Add the matched content object + result.push(match.matcher.contentFactory(match.content)); + // Update currentIndex to the end of the end of the match + // And continue with the search after the end of the match + currentIndex += match.index + match.content.length; + } + + return result; +} + +export function findFirstMatch(contentMatchers: ResponseContentMatcher[], text: string): Match | undefined { + let firstMatch: { matcher: ResponseContentMatcher, index: number, content: string } | undefined; + for (const matcher of contentMatchers) { + const startMatch = matcher.start.exec(text); + if (!startMatch) { + // No start match found, try next matcher. + continue; + } + const endOfStartMatch = startMatch.index + startMatch[0].length; + if (endOfStartMatch >= text.length) { + // There is no text after the start match. + // No need to search for the end match yet, try next matcher. + continue; + } + const remainingTextAfterStartMatch = text.substring(endOfStartMatch); + const endMatch = matcher.end.exec(remainingTextAfterStartMatch); + if (!endMatch) { + // No end match found, try next matcher. + continue; + } + // Found start and end match. + // Record the full match, if it is the earliest found so far. + const index = startMatch.index; + const contentEnd = index + startMatch[0].length + endMatch.index + endMatch[0].length; + const content = text.substring(index, contentEnd); + if (!firstMatch || index < firstMatch.index) { + firstMatch = { matcher, index, content }; + } + } + return firstMatch; +} + diff --git a/packages/ai-chat/src/common/parsed-chat-request.ts b/packages/ai-chat/src/common/parsed-chat-request.ts new file mode 100644 index 0000000000000..816e3fd8dffd8 --- /dev/null +++ b/packages/ai-chat/src/common/parsed-chat-request.ts @@ -0,0 +1,112 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/editor/common/core/offsetRange.ts + +import { AIVariable, ResolvedAIVariable, ToolRequest, toolRequestToPromptText } from '@theia/ai-core'; +import { ChatRequest } from './chat-model'; + +export const chatVariableLeader = '#'; +export const chatAgentLeader = '@'; +export const chatFunctionLeader = '~'; +export const chatSubcommandLeader = '/'; + +/********************** + * INTERFACES AND TYPE GUARDS + **********************/ + +export interface OffsetRange { + readonly start: number; + readonly endExclusive: number; +} + +export interface ParsedChatRequest { + readonly request: ChatRequest; + readonly parts: ParsedChatRequestPart[]; + readonly toolRequests: Map; + readonly variables: Map; +} + +export interface ParsedChatRequestPart { + readonly kind: string; + /** + * The text as represented in the ChatRequest + */ + readonly text: string; + /** + * The text as will be sent to the LLM + */ + readonly promptText: string; + + readonly range: OffsetRange; +} + +export class ParsedChatRequestTextPart implements ParsedChatRequestPart { + readonly kind: 'text'; + + constructor(readonly range: OffsetRange, readonly text: string) { } + + get promptText(): string { + return this.text; + } +} + +export class ParsedChatRequestVariablePart implements ParsedChatRequestPart { + readonly kind: 'var'; + + public resolution: ResolvedAIVariable; + + constructor(readonly range: OffsetRange, readonly variableName: string, readonly variableArg: string | undefined) { } + + get text(): string { + const argPart = this.variableArg ? `:${this.variableArg}` : ''; + return `${chatVariableLeader}${this.variableName}${argPart}`; + } + + get promptText(): string { + return this.resolution?.value ?? this.text; + } +} + +export class ParsedChatRequestFunctionPart implements ParsedChatRequestPart { + readonly kind: 'function'; + constructor(readonly range: OffsetRange, readonly toolRequest: ToolRequest) { } + + get text(): string { + return `${chatFunctionLeader}${this.toolRequest.id}`; + } + + get promptText(): string { + return toolRequestToPromptText(this.toolRequest); + } +} + +export class ParsedChatRequestAgentPart implements ParsedChatRequestPart { + readonly kind: 'agent'; + constructor(readonly range: OffsetRange, readonly agentId: string, readonly agentName: string) { } + + get text(): string { + return `${chatAgentLeader}${this.agentName}`; + } + + get promptText(): string { + return ''; + } +} diff --git a/packages/ai-chat/src/common/response-content-matcher.ts b/packages/ai-chat/src/common/response-content-matcher.ts new file mode 100644 index 0000000000000..3fb785e603c5f --- /dev/null +++ b/packages/ai-chat/src/common/response-content-matcher.ts @@ -0,0 +1,102 @@ +/* + * 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, + CodeChatResponseContentImpl, + MarkdownChatResponseContentImpl +} from './chat-model'; +import { injectable } from '@theia/core/shared/inversify'; + +export type ResponseContentFactory = (content: string) => ChatResponseContent; + +export const MarkdownContentFactory: ResponseContentFactory = (content: string) => + new MarkdownChatResponseContentImpl(content); + +/** + * Default response content factory used if no other `ResponseContentMatcher` applies. + * By default, this factory creates a markdown content object. + * + * @see MarkdownChatResponseContentImpl + */ +@injectable() +export class DefaultResponseContentFactory { + create(content: string): ChatResponseContent { + return MarkdownContentFactory(content); + } +} + +/** + * Clients can contribute response content matchers to parse a chat response into specific + * `ChatResponseContent` instances. + */ +export interface ResponseContentMatcher { + /** Regular expression for finding the start delimiter. */ + start: RegExp; + /** Regular expression for finding the start delimiter. */ + end: RegExp; + /** + * The factory creating a response content from the matching content, + * from start index to end index of the match (including delimiters). + */ + contentFactory: ResponseContentFactory; +} + +export const CodeContentMatcher: ResponseContentMatcher = { + start: /^```.*?$/m, + end: /^```$/m, + contentFactory: (content: string) => { + const language = content.match(/^```(\w+)/)?.[1] || ''; + const code = content.replace(/^```(\w+)\n|```$/g, ''); + return new CodeChatResponseContentImpl(code.trim(), language); + } +}; + +/** + * Clients can contribute response content matchers to parse the response content. + * + * The default chat user interface will collect all contributed matchers and use them + * to parse the response into structured content parts (e.g. code blocks, markdown blocks), + * which are then rendered with a `ChatResponsePartRenderer` registered for the respective + * content part type. + * + * ### Example + * ```ts + * bind(ResponseContentMatcherProvider).to(MyResponseContentMatcherProvider); + * ... + * @injectable() + * export class MyResponseContentMatcherProvider implements ResponseContentMatcherProvider { + * readonly matchers: ResponseContentMatcher[] = [{ + * start: /^$/m, + * end: /^$/m, + * contentFactory: (content: string) => { + * const command = content.replace(/^\n|<\/command>$/g, ''); + * return new MyChatResponseContentImpl(command.trim()); + * } + * }]; + * } + * ``` + * + * @see ResponseContentMatcher + */ +export const ResponseContentMatcherProvider = Symbol('ResponseContentMatcherProvider'); +export interface ResponseContentMatcherProvider { + readonly matchers: ResponseContentMatcher[]; +} + +@injectable() +export class DefaultResponseContentMatcherProvider implements ResponseContentMatcherProvider { + readonly matchers: ResponseContentMatcher[] = [CodeContentMatcher]; +} diff --git a/packages/ai-chat/src/common/universal-chat-agent.ts b/packages/ai-chat/src/common/universal-chat-agent.ts new file mode 100644 index 0000000000000..ab8838d7578e6 --- /dev/null +++ b/packages/ai-chat/src/common/universal-chat-agent.ts @@ -0,0 +1,109 @@ +// ***************************************************************************** +// 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 { AgentSpecificVariables } from '@theia/ai-core'; +import { + PromptTemplate +} from '@theia/ai-core/lib/common'; +import { injectable } from '@theia/core/shared/inversify'; +import { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } from './chat-agents'; + +export const universalTemplate: PromptTemplate = { + id: 'universal-system', + template: `# Instructions + +You are an AI assistant integrated into the Theia IDE, specifically designed to help software developers by +providing concise and accurate answers to programming-related questions. Your role is to enhance the +developer's productivity by offering quick solutions, explanations, and best practices. +Keep responses short and to the point, focusing on delivering valuable insights, best practices and +simple solutions. + +### Guidelines + +1. **Understand Context:** + - Assess the context of the code or issue when available. + - Tailor responses to be relevant to the programming language, framework, or tools like Eclipse Theia. + - Ask clarifying questions if necessary to provide accurate assistance. + +2. **Provide Clear Solutions:** + - Offer direct answers or code snippets that solve the problem or clarify the concept. + - Avoid lengthy explanations unless necessary for understanding. + +3. **Promote Best Practices:** + - Suggest best practices and common patterns relevant to the question. + - Provide links to official documentation for further reading when applicable. + +4. **Support Multiple Languages and Tools:** + - Be familiar with popular programming languages, frameworks, IDEs like Eclipse Theia, and command-line tools. + - Adapt advice based on the language, environment, or tools specified by the developer. + +5. **Facilitate Learning:** + - Encourage learning by explaining why a solution works or why a particular approach is recommended. + - Keep explanations concise and educational. + +6. **Maintain Professional Tone:** + - Communicate in a friendly, professional manner. + - Use technical jargon appropriately, ensuring clarity for the target audience. + +7. **Stay on Topic:** + - Limit responses strictly to topics related to software development, frameworks, Eclipse Theia, terminal usage, and relevant technologies. + - Politely decline to answer questions unrelated to these areas by saying, "I'm here to assist with programming-related questions. + For other topics, please refer to a specialized source." + +### Example Interactions + +- **Question:** "What's the difference between \`let\` and \`var\` in JavaScript?" + **Answer:** "\`let\` is block-scoped, while \`var\` is function-scoped. Prefer \`let\` to avoid scope-related bugs." + +- **Question:** "How do I handle exceptions in Java?" + **Answer:** "Use try-catch blocks: \`\`\`java try { /* code */ } catch (ExceptionType e) { /* handle exception */ }\`\`\`." + +- **Question:** "What is the capital of France?" + **Answer:** "I'm here to assist with programming-related queries. For other topics, please refer to a specialized source." +` +}; + +@injectable() +export class UniversalChatAgent extends AbstractStreamParsingChatAgent implements ChatAgent { + name: string; + description: string; + variables: string[]; + promptTemplates: PromptTemplate[]; + readonly functions: string[]; + readonly agentSpecificVariables: AgentSpecificVariables[]; + + constructor() { + super('Universal', [{ + purpose: 'chat', + identifier: 'openai/gpt-4o', + }], 'chat'); + this.name = 'Universal'; + this.description = 'This agent is designed to help software developers by providing concise and accurate ' + + 'answers to general programming and software development questions. It is also the fall-back for any generic ' + + 'questions the user might ask. The universal agent currently does not have any context by default, i.e. it cannot ' + + 'access the current user context or the workspace.'; + this.variables = []; + this.promptTemplates = [universalTemplate]; + this.functions = []; + this.agentSpecificVariables = []; + } + + protected override async getSystemMessageDescription(): Promise { + const resolvedPrompt = await this.promptService.getPrompt(universalTemplate.id); + return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined; + } + +} diff --git a/packages/ai-chat/tsconfig.json b/packages/ai-chat/tsconfig.json new file mode 100644 index 0000000000000..e7d3cda9e5fdb --- /dev/null +++ b/packages/ai-chat/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../ai-history" + }, + { + "path": "../core" + }, + { + "path": "../filesystem" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/ai-code-completion/.eslintrc.js b/packages/ai-code-completion/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-code-completion/.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-code-completion/README.md b/packages/ai-code-completion/README.md new file mode 100644 index 0000000000000..938ca2c78ffd9 --- /dev/null +++ b/packages/ai-code-completion/README.md @@ -0,0 +1,30 @@ +
+ +
+ +theia-ext-logo + +

ECLIPSE THEIA - AI Code Completion

+ +
+ +
+ +## Description + +The `@theia/ai-code-completion` extension contributes Ai based code completion. +The user can separately enable code completion items as well as inline code completion. + +## 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-code-completion/package.json b/packages/ai-code-completion/package.json new file mode 100644 index 0000000000000..8cf5e1a630163 --- /dev/null +++ b/packages/ai-code-completion/package.json @@ -0,0 +1,54 @@ +{ + "name": "@theia/ai-code-completion", + "version": "1.54.0", + "description": "Theia - AI Core", + "dependencies": { + "@theia/ai-core": "1.54.0", + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", + "@theia/output": "1.54.0", + "@theia/workspace": "1.54.0", + "minimatch": "^5.1.0", + "tslib": "^2.6.2" + }, + "main": "lib/common", + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/ai-code-completion-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-code-completion/src/browser/ai-code-completion-frontend-module.ts b/packages/ai-code-completion/src/browser/ai-code-completion-frontend-module.ts new file mode 100644 index 0000000000000..7a06ea5f2be97 --- /dev/null +++ b/packages/ai-code-completion/src/browser/ai-code-completion-frontend-module.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// 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 { ILogger } from '@theia/core'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { CodeCompletionAgent, CodeCompletionAgentImpl } from '../common/code-completion-agent'; +import { AIFrontendApplicationContribution } from './ai-code-frontend-application-contribution'; +import { FrontendApplicationContribution, PreferenceContribution } from '@theia/core/lib/browser'; +import { Agent } from '@theia/ai-core'; +import { AICodeCompletionPreferencesSchema } from './ai-code-completion-preference'; +import { AICodeInlineCompletionsProvider } from './ai-code-inline-completion-provider'; + +export default new ContainerModule(bind => { + bind(ILogger).toDynamicValue(ctx => { + const parentLogger = ctx.container.get(ILogger); + return parentLogger.child('code-completion-agent'); + }).inSingletonScope().whenTargetNamed('code-completion-agent'); + bind(CodeCompletionAgentImpl).toSelf().inSingletonScope(); + bind(CodeCompletionAgent).toService(CodeCompletionAgentImpl); + bind(Agent).toService(CodeCompletionAgentImpl); + bind(AICodeInlineCompletionsProvider).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).to(AIFrontendApplicationContribution).inSingletonScope(); + bind(PreferenceContribution).toConstantValue({ schema: AICodeCompletionPreferencesSchema }); +}); diff --git a/packages/ai-code-completion/src/browser/ai-code-completion-preference.ts b/packages/ai-code-completion/src/browser/ai-code-completion-preference.ts new file mode 100644 index 0000000000000..60cab4a55a59b --- /dev/null +++ b/packages/ai-code-completion/src/browser/ai-code-completion-preference.ts @@ -0,0 +1,42 @@ +// ***************************************************************************** +// 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 { PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; +import { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/browser/ai-core-preferences'; + +export const PREF_AI_INLINE_COMPLETION_ENABLE = 'ai-features.codeCompletion.enableCodeCompletion'; +export const PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS = 'ai-features.codeCompletion.excludedFileExtensions'; + +export const AICodeCompletionPreferencesSchema: PreferenceSchema = { + type: 'object', + properties: { + [PREF_AI_INLINE_COMPLETION_ENABLE]: { + title: AI_CORE_PREFERENCES_TITLE, + type: 'boolean', + description: 'Enable AI completions inline within any (Monaco) editor.', + default: false + }, + [PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS]: { + title: 'Excluded File Extensions', + type: 'array', + description: 'Specify file extensions (e.g., .md, .txt) where AI completions should be disabled.', + items: { + type: 'string' + }, + default: [] + } + } +}; diff --git a/packages/ai-code-completion/src/browser/ai-code-frontend-application-contribution.ts b/packages/ai-code-completion/src/browser/ai-code-frontend-application-contribution.ts new file mode 100644 index 0000000000000..9d7c610979606 --- /dev/null +++ b/packages/ai-code-completion/src/browser/ai-code-frontend-application-contribution.ts @@ -0,0 +1,89 @@ +// ***************************************************************************** +// 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 * as monaco from '@theia/monaco-editor-core'; + +import { FrontendApplicationContribution, PreferenceService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { AIActivationService } from '@theia/ai-core/lib/browser'; +import { Disposable } from '@theia/core'; +import { AICodeInlineCompletionsProvider } from './ai-code-inline-completion-provider'; +import { PREF_AI_INLINE_COMPLETION_ENABLE, PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS } from './ai-code-completion-preference'; + +@injectable() +export class AIFrontendApplicationContribution implements FrontendApplicationContribution { + @inject(AICodeInlineCompletionsProvider) + private inlineCodeCompletionProvider: AICodeInlineCompletionsProvider; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + @inject(AIActivationService) + protected readonly activationService: AIActivationService; + + private toDispose = new Map(); + + onDidInitializeLayout(): void { + this.preferenceService.ready.then(() => { + this.handlePreferences(); + }); + } + + protected handlePreferences(): void { + const handler = () => this.handleInlineCompletions(); + + this.toDispose.set('inlineCompletions', handler()); + + this.preferenceService.onPreferenceChanged(event => { + if (event.preferenceName === PREF_AI_INLINE_COMPLETION_ENABLE || event.preferenceName === PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS) { + this.toDispose.get('inlineCompletions')?.dispose(); + this.toDispose.set('inlineCompletions', handler()); + } + }); + + this.activationService.onDidChangeActiveStatus(change => { + this.toDispose.get('inlineCompletions')?.dispose(); + this.toDispose.set('inlineCompletions', handler()); + }); + } + + protected handleInlineCompletions(): Disposable { + const enable = this.preferenceService.get(PREF_AI_INLINE_COMPLETION_ENABLE, false) && this.activationService.isActive; + + if (!enable) { + return Disposable.NULL; + } + + const excludedExtensions = this.preferenceService.get(PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS, []); + + return monaco.languages.registerInlineCompletionsProvider( + { scheme: 'file' }, + { + provideInlineCompletions: (model, position, context, token) => { + const fileName = model.uri.toString(); + + if (excludedExtensions.some(ext => fileName.endsWith(ext))) { + return { items: [] }; + } + return this.inlineCodeCompletionProvider.provideInlineCompletions(model, position, context, token); + }, + freeInlineCompletions: completions => { + this.inlineCodeCompletionProvider.freeInlineCompletions(completions); + } + } + ); + } +} diff --git a/packages/ai-code-completion/src/browser/ai-code-inline-completion-provider.ts b/packages/ai-code-completion/src/browser/ai-code-inline-completion-provider.ts new file mode 100644 index 0000000000000..b2bb35af85533 --- /dev/null +++ b/packages/ai-code-completion/src/browser/ai-code-inline-completion-provider.ts @@ -0,0 +1,53 @@ +// ***************************************************************************** +// 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 * as monaco from '@theia/monaco-editor-core'; + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { CodeCompletionAgent } from '../common/code-completion-agent'; +import { AgentService } from '@theia/ai-core'; + +@injectable() +export class AICodeInlineCompletionsProvider + implements monaco.languages.InlineCompletionsProvider { + @inject(CodeCompletionAgent) + protected readonly agent: CodeCompletionAgent; + @inject(AgentService) + private readonly agentService: AgentService; + + async provideInlineCompletions( + model: monaco.editor.ITextModel, + position: monaco.Position, + context: monaco.languages.InlineCompletionContext, + token: monaco.CancellationToken + ): Promise { + if (!this.agentService.isEnabled(this.agent.id)) { + return undefined; + } + return this.agent.provideInlineCompletions( + model, + position, + context, + token + ); + } + + freeInlineCompletions( + completions: monaco.languages.InlineCompletions + ): void { + // nothing to do + } +} diff --git a/packages/ai-code-completion/src/common/code-completion-agent.ts b/packages/ai-code-completion/src/common/code-completion-agent.ts new file mode 100644 index 0000000000000..15724190118f0 --- /dev/null +++ b/packages/ai-code-completion/src/common/code-completion-agent.ts @@ -0,0 +1,164 @@ +// ***************************************************************************** +// 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 { + Agent, AgentSpecificVariables, CommunicationHistoryEntry, CommunicationRecordingService, getTextOfResponse, + LanguageModelRegistry, LanguageModelRequest, LanguageModelRequirement, PromptService, PromptTemplate +} from '@theia/ai-core/lib/common'; +import { generateUuid, ILogger } from '@theia/core'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import * as monaco from '@theia/monaco-editor-core'; + +export const CodeCompletionAgent = Symbol('CodeCompletionAgent'); +export interface CodeCompletionAgent extends Agent { + provideInlineCompletions(model: monaco.editor.ITextModel, position: monaco.Position, + context: monaco.languages.InlineCompletionContext, token: monaco.CancellationToken): Promise +} + +@injectable() +export class CodeCompletionAgentImpl implements CodeCompletionAgent { + async provideInlineCompletions( + model: monaco.editor.ITextModel, + position: monaco.Position, + context: monaco.languages.InlineCompletionContext, + token: monaco.CancellationToken + ): Promise { + const languageModel = + await this.languageModelRegistry.selectLanguageModel({ + agent: this.id, + ...this.languageModelRequirements[0], + }); + if (!languageModel) { + this.logger.error( + 'No language model found for code-completion-agent' + ); + return undefined; + } + + // Get text until the given position + const textUntilCurrentPosition = model.getValueInRange({ + startLineNumber: 1, + startColumn: 1, + endLineNumber: position.lineNumber, + endColumn: position.column, + }); + + // Get text after the given position + const textAfterCurrentPosition = model.getValueInRange({ + startLineNumber: position.lineNumber, + startColumn: position.column, + endLineNumber: model.getLineCount(), + endColumn: model.getLineMaxColumn(model.getLineCount()), + }); + + const file = model.uri.toString(false); + const language = model.getLanguageId(); + + if (token.isCancellationRequested) { + return undefined; + } + const prompt = await this.promptService + .getPrompt('code-completion-prompt', { textUntilCurrentPosition, textAfterCurrentPosition, file, language }) + .then(p => p?.text); + if (!prompt) { + this.logger.error('No prompt found for code-completion-agent'); + return undefined; + } + + // since we do not actually hold complete conversions, the request/response pair is considered a session + const sessionId = generateUuid(); + const requestId = generateUuid(); + const request: LanguageModelRequest = { + messages: [{ type: 'text', actor: 'user', query: prompt }], + }; + const requestEntry: CommunicationHistoryEntry = { + agentId: this.id, + sessionId, + timestamp: Date.now(), + requestId, + request: prompt, + }; + if (token.isCancellationRequested) { + return undefined; + } + this.recordingService.recordRequest(requestEntry); + const response = await languageModel.request(request, token); + if (token.isCancellationRequested) { + return undefined; + } + const completionText = await getTextOfResponse(response); + if (token.isCancellationRequested) { + return undefined; + } + this.recordingService.recordResponse({ + agentId: this.id, + sessionId, + timestamp: Date.now(), + requestId, + response: completionText, + }); + + return { + items: [{ insertText: completionText }], + enableForwardStability: true, + }; + } + + @inject(ILogger) + @named('code-completion-agent') + protected logger: ILogger; + + @inject(LanguageModelRegistry) + protected languageModelRegistry: LanguageModelRegistry; + + @inject(PromptService) + protected promptService: PromptService; + + @inject(CommunicationRecordingService) + protected recordingService: CommunicationRecordingService; + + id = 'Code Completion'; + name = 'Code Completion'; + description = + 'This agent provides inline code completion in the code editor in the Theia IDE.'; + promptTemplates: PromptTemplate[] = [ + { + id: 'code-completion-prompt', + template: `You are a code completion agent. The current file you have to complete is named {{file}}. +The language of the file is {{language}}. Return your result as plain text without markdown formatting. +Finish the following code snippet. + +{{textUntilCurrentPosition}}[[MARKER]]{{textAfterCurrentPosition}} + +Only return the exact replacement for [[MARKER]] to complete the snippet.`, + }, + ]; + languageModelRequirements: LanguageModelRequirement[] = [ + { + purpose: 'code-completion', + identifier: 'openai/gpt-4o', + }, + ]; + readonly variables: string[] = []; + readonly functions: string[] = []; + readonly agentSpecificVariables: AgentSpecificVariables[] = [ + { name: 'file', usedInPrompt: true, description: 'The uri of the file being edited.' }, + { name: 'language', usedInPrompt: true, description: 'The languageId of the file being edited.' }, + { name: 'textUntilCurrentPosition', usedInPrompt: true, description: 'The code before the current position of the cursor.' }, + { name: 'textAfterCurrentPosition', usedInPrompt: true, description: 'The code after the current position of the cursor.' } + ]; + readonly tags?: String[] | undefined; +} diff --git a/packages/ai-code-completion/src/package.spec.ts b/packages/ai-code-completion/src/package.spec.ts new file mode 100644 index 0000000000000..fec76f95059b4 --- /dev/null +++ b/packages/ai-code-completion/src/package.spec.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource 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 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('ai-code-completion package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-code-completion/tsconfig.json b/packages/ai-code-completion/tsconfig.json new file mode 100644 index 0000000000000..548b369565b41 --- /dev/null +++ b/packages/ai-code-completion/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../filesystem" + }, + { + "path": "../output" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/ai-core/.eslintrc.js b/packages/ai-core/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-core/.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-core/README.md b/packages/ai-core/README.md new file mode 100644 index 0000000000000..1cd399aaff0e1 --- /dev/null +++ b/packages/ai-core/README.md @@ -0,0 +1,30 @@ +
+ +
+ +theia-ext-logo + +

ECLIPSE THEIA - AI Core EXTENSION

+ +
+ +
+ +## Description + +The `@theia/ai-core` extension serves as the basis of all AI integration in Theia. +It manages the integration of language models and provides core concepts like agents, prompts and AI variables. + +## 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-core/data/prompttemplate.tmLanguage.json b/packages/ai-core/data/prompttemplate.tmLanguage.json new file mode 100644 index 0000000000000..da9b33b66723a --- /dev/null +++ b/packages/ai-core/data/prompttemplate.tmLanguage.json @@ -0,0 +1,52 @@ +{ + "scopeName": "source.prompttemplate", + "patterns": [ + { + "name": "variable.other.prompttemplate", + "begin": "{{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.brace.begin" + } + }, + "end": "}}", + "endCaptures": { + "0": { + "name": "punctuation.definition.brace.end" + } + }, + "patterns": [ + { + "name": "keyword.control", + "match": "[a-zA-Z_][a-zA-Z0-9_]*" + } + ] + }, + { + "name": "support.function.prompttemplate", + "begin": "~{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.brace.begin" + } + }, + "end": "}", + "endCaptures": { + "0": { + "name": "punctuation.definition.brace.end" + } + }, + "patterns": [ + { + "name": "keyword.control", + "match": "[a-zA-Z_][a-zA-Z0-9_\\-]*" + } + ] + } + ], + "repository": {}, + "name": "PromptTemplate", + "fileTypes": [ + ".prompttemplate" + ] +} diff --git a/packages/ai-core/package.json b/packages/ai-core/package.json new file mode 100644 index 0000000000000..88a834aa8ab06 --- /dev/null +++ b/packages/ai-core/package.json @@ -0,0 +1,60 @@ +{ + "name": "@theia/ai-core", + "version": "1.54.0", + "description": "Theia - AI Core", + "dependencies": { + "@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/output": "1.54.0", + "@theia/variable-resolver": "1.54.0", + "@theia/workspace": "1.54.0", + "@types/js-yaml": "^4.0.9", + "js-yaml": "^4.1.0", + "minimatch": "^5.1.0", + "tslib": "^2.6.2" + }, + "main": "lib/common", + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/ai-core-frontend-module", + "backend": "lib/node/ai-core-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": [ + "data", + "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-core/src/browser/ai-activation-service.ts b/packages/ai-core/src/browser/ai-activation-service.ts new file mode 100644 index 0000000000000..c26c8c1d15e53 --- /dev/null +++ b/packages/ai-core/src/browser/ai-activation-service.ts @@ -0,0 +1,56 @@ +// ***************************************************************************** +// 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 { FrontendApplicationContribution, PreferenceService } from '@theia/core/lib/browser'; +import { Emitter, MaybePromise, Event, } from '@theia/core'; +import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service'; +import { PREFERENCE_NAME_ENABLE_EXPERIMENTAL } from './ai-core-preferences'; + +/** + * Context key for the experimental AI feature. It is set to `true` if the feature is enabled. + */ +// We reuse the enablement preference for the context key +export const EXPERIMENTAL_AI_CONTEXT_KEY = PREFERENCE_NAME_ENABLE_EXPERIMENTAL; + +@injectable() +export class AIActivationService implements FrontendApplicationContribution { + @inject(ContextKeyService) + protected readonly contextKeyService: ContextKeyService; + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + protected isExperimentalEnabledKey: ContextKey; + + protected onDidChangeExperimental = new Emitter(); + get onDidChangeActiveStatus(): Event { + return this.onDidChangeExperimental.event; + } + + get isActive(): boolean { + return this.isExperimentalEnabledKey.get() ?? false; + } + + initialize(): MaybePromise { + this.isExperimentalEnabledKey = this.contextKeyService.createKey(EXPERIMENTAL_AI_CONTEXT_KEY, false); + this.preferenceService.onPreferenceChanged(e => { + if (e.preferenceName === PREFERENCE_NAME_ENABLE_EXPERIMENTAL) { + this.isExperimentalEnabledKey.set(e.newValue); + this.onDidChangeExperimental.fire(e.newValue); + } + }); + } +} diff --git a/packages/ai-core/src/browser/ai-command-handler-factory.ts b/packages/ai-core/src/browser/ai-command-handler-factory.ts new file mode 100644 index 0000000000000..f5f82856be386 --- /dev/null +++ b/packages/ai-core/src/browser/ai-command-handler-factory.ts @@ -0,0 +1,20 @@ +// ***************************************************************************** +// 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 { CommandHandler } from '@theia/core'; + +export type AICommandHandlerFactory = (handler: CommandHandler) => CommandHandler; +export const AICommandHandlerFactory = Symbol('AICommandHandlerFactory'); diff --git a/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx b/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx new file mode 100644 index 0000000000000..9a8798e584986 --- /dev/null +++ b/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx @@ -0,0 +1,307 @@ +// ***************************************************************************** +// 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 { codicon, ReactWidget } from '@theia/core/lib/browser'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; +import { + Agent, + AISettingsService, + AIVariableService, + LanguageModel, + LanguageModelRegistry, + PROMPT_FUNCTION_REGEX, + PROMPT_VARIABLE_REGEX, + PromptCustomizationService, + PromptService, +} from '../../common'; +import { LanguageModelRenderer } from './language-model-renderer'; +import { TemplateRenderer } from './template-settings-renderer'; +import { AIConfigurationSelectionService } from './ai-configuration-service'; +import { AIVariableConfigurationWidget } from './variable-configuration-widget'; +import { AgentService } from '../../common/agent-service'; + +interface ParsedPrompt { + functions: string[]; + globalVariables: string[]; + agentSpecificVariables: string[]; +}; + +@injectable() +export class AIAgentConfigurationWidget extends ReactWidget { + + static readonly ID = 'ai-agent-configuration-container-widget'; + static readonly LABEL = 'Agents'; + + @inject(AgentService) + protected readonly agentService: AgentService; + + @inject(LanguageModelRegistry) + protected readonly languageModelRegistry: LanguageModelRegistry; + + @inject(PromptCustomizationService) + protected readonly promptCustomizationService: PromptCustomizationService; + + @inject(AISettingsService) + protected readonly aiSettingsService: AISettingsService; + + @inject(AIConfigurationSelectionService) + protected readonly aiConfigurationSelectionService: AIConfigurationSelectionService; + + @inject(AIVariableService) + protected readonly variableService: AIVariableService; + + @inject(PromptService) + protected promptService: PromptService; + + protected languageModels: LanguageModel[] | undefined; + + @postConstruct() + protected init(): void { + this.id = AIAgentConfigurationWidget.ID; + this.title.label = AIAgentConfigurationWidget.LABEL; + this.title.closable = false; + + this.languageModelRegistry.getLanguageModels().then(models => { + this.languageModels = models ?? []; + this.update(); + }); + this.toDispose.push(this.languageModelRegistry.onChange(({ models }) => { + this.languageModels = models; + this.update(); + })); + this.toDispose.push(this.promptCustomizationService.onDidChangePrompt(() => this.update())); + + this.aiSettingsService.onDidChange(() => this.update()); + this.aiConfigurationSelectionService.onDidAgentChange(() => this.update()); + this.agentService.onDidChangeAgents(() => this.update()); + this.update(); + } + + protected render(): React.ReactNode { + return
+
+
    + {this.agentService.getAllAgents().map(agent => +
  • this.setActiveAgent(agent)}> + {this.renderAgentName(agent)} +
  • + )} +
+
+ +
+
+
+ {this.renderAgentDetails()} +
+
; + } + + private renderAgentName(agent: Agent): React.ReactNode { + const tagsSuffix = agent.tags?.length ? {agent.tags.map(tag => {tag})} : ''; + return {agent.name} {tagsSuffix}; + } + + private renderAgentDetails(): React.ReactNode { + const agent = this.aiConfigurationSelectionService.getActiveAgent(); + if (!agent) { + return
Please select an Agent first!
; + } + + const enabled = this.agentService.isEnabled(agent.id); + + const parsedPromptParts = this.parsePromptTemplatesForVariableAndFunction(agent); + const globalVariables = Array.from(new Set([...parsedPromptParts.globalVariables, ...agent.variables])); + const functions = Array.from(new Set([...parsedPromptParts.functions, ...agent.functions])); + + return
+
{this.renderAgentName(agent)}
+
{agent.description}
+
+ +
+
+ {agent.promptTemplates?.map(template => + )} +
+
+ +
+
+ Used Global Variables: +
    + +
+
+
+ Used agent-specific Variables: +
    + +
+
+
+ Used Functions: +
    + +
+
+
; + } + + private parsePromptTemplatesForVariableAndFunction(agent: Agent): ParsedPrompt { + const promptTemplates = agent.promptTemplates; + const result: ParsedPrompt = { functions: [], globalVariables: [], agentSpecificVariables: [] }; + promptTemplates.forEach(template => { + const storedPrompt = this.promptService.getRawPrompt(template.id); + const prompt = storedPrompt?.template ?? template.template; + const variableMatches = [...prompt.matchAll(PROMPT_VARIABLE_REGEX)]; + + variableMatches.forEach(match => { + const variableId = match[1]; + // if the variable is part of the variable service and not part of the agent specific variables then it is a global variable + if (this.variableService.hasVariable(variableId) && + agent.agentSpecificVariables.find(v => v.name === variableId) === undefined) { + result.globalVariables.push(variableId); + } else { + result.agentSpecificVariables.push(variableId); + } + }); + + const functionMatches = [...prompt.matchAll(PROMPT_FUNCTION_REGEX)]; + functionMatches.forEach(match => { + const functionId = match[1]; + result.functions.push(functionId); + }); + + }); + return result; + } + + protected showVariableConfigurationTab(): void { + this.aiConfigurationSelectionService.selectConfigurationTab(AIVariableConfigurationWidget.ID); + } + + protected addCustomAgent(): void { + this.promptCustomizationService.openCustomAgentYaml(); + } + + protected setActiveAgent(agent: Agent): void { + this.aiConfigurationSelectionService.setActiveAgent(agent); + this.update(); + } + + private toggleAgentEnabled = () => { + const agent = this.aiConfigurationSelectionService.getActiveAgent(); + if (!agent) { + return false; + } + const enabled = this.agentService.isEnabled(agent.id); + if (enabled) { + this.agentService.disableAgent(agent.id); + } else { + this.agentService.enableAgent(agent.id); + } + this.update(); + }; + +} +interface AgentGlobalVariablesProps { + variables: string[]; + showVariableConfigurationTab: () => void; +} +const AgentGlobalVariables = ({ variables: globalVariables, showVariableConfigurationTab }: AgentGlobalVariablesProps) => { + if (globalVariables.length === 0) { + return <>None; + } + return <> + {globalVariables.map(variableId =>
  • +
    { showVariableConfigurationTab(); }} className='variable-reference'> + {variableId} + +
  • )} + + ; +}; + +interface AgentFunctionsProps { + functions: string[]; +} +const AgentFunctions = ({ functions }: AgentFunctionsProps) => { + if (functions.length === 0) { + return <>None; + } + return <> + {functions.map(functionId =>
  • + {functionId} +
  • )} + ; +}; + +interface AgentSpecificVariablesProps { + promptVariables: string[]; + agent: Agent; +} +const AgentSpecificVariables = ({ promptVariables, agent }: AgentSpecificVariablesProps) => { + const agentDefinedVariablesName = agent.agentSpecificVariables.map(v => v.name); + const variables = Array.from(new Set([...promptVariables, ...agentDefinedVariablesName])); + if (variables.length === 0) { + return <>None; + } + return <> + {variables.map(variableId => + + + )} + ; +}; +interface AgentSpecifcVariableProps { + variableId: string; + agent: Agent; + promptVariables: string[]; +} +const AgentSpecifcVariable = ({ variableId, agent, promptVariables }: AgentSpecifcVariableProps) => { + const agentDefinedVariable = agent.agentSpecificVariables.find(v => v.name === variableId); + const undeclared = agentDefinedVariable === undefined; + const notUsed = !promptVariables.includes(variableId) && agentDefinedVariable?.usedInPrompt === true; + return
  • +
    Name: {variableId}
    + {undeclared ?
    Undeclared
    : + (<> +
    Description: {agentDefinedVariable.description}
    + {notUsed &&
    Not used in prompt
    } + )} +
    +
  • ; +}; diff --git a/packages/ai-core/src/browser/ai-configuration/ai-configuration-service.ts b/packages/ai-core/src/browser/ai-configuration/ai-configuration-service.ts new file mode 100644 index 0000000000000..bd364a9a1d3cd --- /dev/null +++ b/packages/ai-core/src/browser/ai-configuration/ai-configuration-service.ts @@ -0,0 +1,43 @@ +// ***************************************************************************** +// 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 { Emitter } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { Agent } from '../../common'; + +@injectable() +export class AIConfigurationSelectionService { + protected activeAgent?: Agent; + + protected readonly onDidSelectConfigurationEmitter = new Emitter(); + onDidSelectConfiguration = this.onDidSelectConfigurationEmitter.event; + + protected readonly onDidAgentChangeEmitter = new Emitter(); + onDidAgentChange = this.onDidSelectConfigurationEmitter.event; + + public getActiveAgent(): Agent | undefined { + return this.activeAgent; + } + + public setActiveAgent(agent?: Agent): void { + this.activeAgent = agent; + this.onDidAgentChangeEmitter.fire(agent); + } + + public selectConfigurationTab(widgetId: string): void { + this.onDidSelectConfigurationEmitter.fire(widgetId); + } +} diff --git a/packages/ai-core/src/browser/ai-configuration/ai-configuration-view-contribution.ts b/packages/ai-core/src/browser/ai-configuration/ai-configuration-view-contribution.ts new file mode 100644 index 0000000000000..4e6e371240f46 --- /dev/null +++ b/packages/ai-core/src/browser/ai-configuration/ai-configuration-view-contribution.ts @@ -0,0 +1,54 @@ +// ***************************************************************************** +// 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 { FrontendApplication } from '@theia/core/lib/browser'; +import { injectable } from '@theia/core/shared/inversify'; +import { AIViewContribution } from '../ai-view-contribution'; +import { AIConfigurationContainerWidget } from './ai-configuration-widget'; +import { Command, CommandRegistry } from '@theia/core'; + +export const AI_CONFIGURATION_TOGGLE_COMMAND_ID = 'aiConfiguration:toggle'; +export const OPEN_AI_CONFIG_VIEW = Command.toLocalizedCommand({ + id: 'aiConfiguration:open', + label: 'Open AI Configuration view', +}); + +@injectable() +export class AIAgentConfigurationViewContribution extends AIViewContribution { + + constructor() { + super({ + widgetId: AIConfigurationContainerWidget.ID, + widgetName: AIConfigurationContainerWidget.LABEL, + defaultWidgetOptions: { + area: 'main', + rank: 100 + }, + toggleCommandId: AI_CONFIGURATION_TOGGLE_COMMAND_ID + }); + } + + async initializeLayout(_app: FrontendApplication): Promise { + await this.openView(); + } + + override registerCommands(commands: CommandRegistry): void { + super.registerCommands(commands); + commands.registerCommand(OPEN_AI_CONFIG_VIEW, { + execute: () => this.openView({ activate: true }), + }); + } +} + diff --git a/packages/ai-core/src/browser/ai-configuration/ai-configuration-widget.tsx b/packages/ai-core/src/browser/ai-configuration/ai-configuration-widget.tsx new file mode 100644 index 0000000000000..909c822d8df47 --- /dev/null +++ b/packages/ai-core/src/browser/ai-configuration/ai-configuration-widget.tsx @@ -0,0 +1,80 @@ +// ***************************************************************************** +// 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 { BaseWidget, BoxLayout, codicon, DockPanel, WidgetManager } from '@theia/core/lib/browser'; +import { TheiaDockPanel } from '@theia/core/lib/browser/shell/theia-dock-panel'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import '../../../src/browser/style/index.css'; +import { AIAgentConfigurationWidget } from './agent-configuration-widget'; +import { AIVariableConfigurationWidget } from './variable-configuration-widget'; +import { AIConfigurationSelectionService } from './ai-configuration-service'; + +@injectable() +export class AIConfigurationContainerWidget extends BaseWidget { + + static readonly ID = 'ai-configuration'; + static readonly LABEL = '✨ AI Configuration [Experimental]'; + protected dockpanel: DockPanel; + + @inject(TheiaDockPanel.Factory) + protected readonly dockPanelFactory: TheiaDockPanel.Factory; + @inject(WidgetManager) + protected readonly widgetManager: WidgetManager; + @inject(AIConfigurationSelectionService) + protected readonly aiConfigurationSelectionService: AIConfigurationSelectionService; + + protected agentsWidget: AIAgentConfigurationWidget; + protected variablesWidget: AIVariableConfigurationWidget; + + @postConstruct() + protected init(): void { + this.id = AIConfigurationContainerWidget.ID; + this.title.label = AIConfigurationContainerWidget.LABEL; + this.title.closable = true; + this.addClass('theia-settings-container'); + this.title.iconClass = codicon('hubot'); + this.initUI(); + this.initListeners(); + } + + protected async initUI(): Promise { + const layout = (this.layout = new BoxLayout({ direction: 'top-to-bottom', spacing: 0 })); + this.dockpanel = this.dockPanelFactory({ + mode: 'multiple-document', + spacing: 0 + }); + BoxLayout.setStretch(this.dockpanel, 1); + layout.addWidget(this.dockpanel); + this.dockpanel.addClass('ai-configuration-widget'); + + this.agentsWidget = await this.widgetManager.getOrCreateWidget(AIAgentConfigurationWidget.ID); + this.variablesWidget = await this.widgetManager.getOrCreateWidget(AIVariableConfigurationWidget.ID); + this.dockpanel.addWidget(this.agentsWidget); + this.dockpanel.addWidget(this.variablesWidget); + + this.update(); + } + + protected initListeners(): void { + this.aiConfigurationSelectionService.onDidSelectConfiguration(widgetId => { + if (widgetId === AIAgentConfigurationWidget.ID) { + this.dockpanel.activateWidget(this.agentsWidget); + } else if (widgetId === AIVariableConfigurationWidget.ID) { + this.dockpanel.activateWidget(this.variablesWidget); + } + }); + } +} diff --git a/packages/ai-core/src/browser/ai-configuration/language-model-renderer.tsx b/packages/ai-core/src/browser/ai-configuration/language-model-renderer.tsx new file mode 100644 index 0000000000000..1305168f65f59 --- /dev/null +++ b/packages/ai-core/src/browser/ai-configuration/language-model-renderer.tsx @@ -0,0 +1,113 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { Agent, LanguageModelRequirement } from '../../common'; +import { LanguageModel, LanguageModelRegistry } from '../../common/language-model'; +import { AISettingsService } from '../../common/settings-service'; +import { Mutable } from '@theia/core'; + +export interface LanguageModelSettingsProps { + agent: Agent; + languageModels?: LanguageModel[]; + aiSettingsService: AISettingsService; + languageModelRegistry: LanguageModelRegistry; +} + +export const LanguageModelRenderer: React.FC = ( + { agent, languageModels, aiSettingsService, languageModelRegistry }) => { + + const findLanguageModelRequirement = async (purpose: string): Promise => { + const requirementSetting = await aiSettingsService.getAgentSettings(agent.id); + return requirementSetting?.languageModelRequirements?.find(e => e.purpose === purpose); + }; + + const [lmRequirementMap, setLmRequirementMap] = React.useState>({}); + + React.useEffect(() => { + const computeLmRequirementMap = async () => { + const map = await agent.languageModelRequirements.reduce(async (accPromise, curr) => { + const acc = await accPromise; + // take the agents requirements and override them with the user settings if present + const lmRequirement = await findLanguageModelRequirement(curr.purpose) ?? curr; + // if no llm is selected through the identifier, see what would be the default + if (!lmRequirement.identifier) { + const llm = await languageModelRegistry.selectLanguageModel({ agent: agent.id, ...lmRequirement }); + (lmRequirement as Mutable).identifier = llm?.id; + } + acc[curr.purpose] = lmRequirement; + return acc; + }, Promise.resolve({} as Record)); + setLmRequirementMap(map); + }; + computeLmRequirementMap(); + }, []); + + const renderLanguageModelMetadata = (requirement: LanguageModelRequirement, index: number) => { + const languageModel = languageModels?.find(model => model.id === requirement.identifier); + if (!languageModel) { + return
    ; + } + + return <> +
    {requirement.purpose}
    +
    + {languageModel.id &&

    Identifier: {languageModel.id}

    } + {languageModel.name &&

    Name: {languageModel.name}

    } + {languageModel.vendor &&

    Vendor: {languageModel.vendor}

    } + {languageModel.version &&

    Version: {languageModel.version}

    } + {languageModel.family &&

    Family: {languageModel.family}

    } + {languageModel.maxInputTokens &&

    Min Input Tokens: {languageModel.maxInputTokens}

    } + {languageModel.maxOutputTokens &&

    Max Output Tokens: {languageModel.maxOutputTokens}

    } +
    + ; + + }; + + const onSelectedModelChange = (purpose: string, event: React.ChangeEvent): void => { + const newLmRequirementMap = { ...lmRequirementMap, [purpose]: { purpose, identifier: event.target.value } }; + aiSettingsService.updateAgentSettings(agent.id, { languageModelRequirements: Object.values(newLmRequirementMap) }); + setLmRequirementMap(newLmRequirementMap); + }; + + return
    + {Object.values(lmRequirementMap).map((requirements, index) => ( + +
    Purpose:
    +
    + {/* language model metadata */} + {renderLanguageModelMetadata(requirements, index)} + {/* language model selector */} + <> + + + +
    +
    +
    + ))} + +
    ; +}; diff --git a/packages/ai-core/src/browser/ai-configuration/template-settings-renderer.tsx b/packages/ai-core/src/browser/ai-configuration/template-settings-renderer.tsx new file mode 100644 index 0000000000000..01125ebf58e0a --- /dev/null +++ b/packages/ai-core/src/browser/ai-configuration/template-settings-renderer.tsx @@ -0,0 +1,39 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { PromptCustomizationService } from '../../common/prompt-service'; +import { PromptTemplate } from '../../common'; + +export interface TemplateSettingProps { + agentId: string; + template: PromptTemplate; + promptCustomizationService: PromptCustomizationService; +} + +export const TemplateRenderer: React.FC = ({ agentId, template, promptCustomizationService }) => { + const openTemplate = React.useCallback(async () => { + promptCustomizationService.editTemplate(template.id); + }, [template, promptCustomizationService]); + const resetTemplate = React.useCallback(async () => { + promptCustomizationService.resetTemplate(template.id); + }, [promptCustomizationService, template]); + + return <> + {template.id} + + + ; +}; diff --git a/packages/ai-core/src/browser/ai-configuration/variable-configuration-widget.tsx b/packages/ai-core/src/browser/ai-configuration/variable-configuration-widget.tsx new file mode 100644 index 0000000000000..64cbdfffe6122 --- /dev/null +++ b/packages/ai-core/src/browser/ai-configuration/variable-configuration-widget.tsx @@ -0,0 +1,110 @@ +// ***************************************************************************** +// 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 { codicon, ReactWidget } from '@theia/core/lib/browser'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; +import { Agent, AIVariable, AIVariableService } from '../../common'; +import { AIAgentConfigurationWidget } from './agent-configuration-widget'; +import { AIConfigurationSelectionService } from './ai-configuration-service'; +import { AgentService } from '../../common/agent-service'; + +@injectable() +export class AIVariableConfigurationWidget extends ReactWidget { + + static readonly ID = 'ai-variable-configuration-container-widget'; + static readonly LABEL = 'Variables'; + + @inject(AIVariableService) + protected readonly variableService: AIVariableService; + + @inject(AgentService) + protected readonly agentService: AgentService; + + @inject(AIConfigurationSelectionService) + protected readonly aiConfigurationSelectionService: AIConfigurationSelectionService; + + @postConstruct() + protected init(): void { + this.id = AIVariableConfigurationWidget.ID; + this.title.label = AIVariableConfigurationWidget.LABEL; + this.title.closable = false; + this.update(); + this.toDispose.push(this.variableService.onDidChangeVariables(() => this.update())); + } + + protected render(): React.ReactNode { + return
    +
      + {this.variableService.getVariables().map(variable => +
    • +
      {variable.name}
      + {variable.id} + {variable.description} + {this.renderReferencedVariables(variable)} + {this.renderArgs(variable)} +
    • + )} +
    +
    ; + } + + protected renderReferencedVariables(variable: AIVariable): React.ReactNode | undefined { + const agents = this.getAgentsForVariable(variable); + if (agents.length === 0) { + return; + } + + return
    +

    Agents

    +
      + {agents.map(agent =>
    • +
      { this.showAgentConfiguration(agent); }} className='variable-reference'> + {agent.name} + +
    • )} +
    +
    ; + } + + protected renderArgs(variable: AIVariable): React.ReactNode | undefined { + if (variable.args === undefined || variable.args.length === 0) { + return; + } + + return
    +

    Variable Arguments

    +
    + {variable.args.map(arg => + + {arg.name} + {arg.description} + + )} +
    +
    ; + } + + protected showAgentConfiguration(agent: Agent): void { + this.aiConfigurationSelectionService.setActiveAgent(agent); + this.aiConfigurationSelectionService.selectConfigurationTab(AIAgentConfigurationWidget.ID); + } + + protected getAgentsForVariable(variable: AIVariable): Agent[] { + return this.agentService.getAgents().filter(a => a.variables?.includes(variable.id)); + } +} + diff --git a/packages/ai-core/src/browser/ai-core-frontend-application-contribution.ts b/packages/ai-core/src/browser/ai-core-frontend-application-contribution.ts new file mode 100644 index 0000000000000..c419620b2401f --- /dev/null +++ b/packages/ai-core/src/browser/ai-core-frontend-application-contribution.ts @@ -0,0 +1,40 @@ +// ***************************************************************************** +// 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 { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { PromptService } from '../common'; +import { AgentService } from '../common/agent-service'; + +@injectable() +export class AICoreFrontendApplicationContribution implements FrontendApplicationContribution { + @inject(AgentService) + private readonly agentService: AgentService; + + @inject(PromptService) + private readonly promptService: PromptService; + + onStart(): void { + this.agentService.getAllAgents().forEach(a => { + a.promptTemplates.forEach(t => { + this.promptService.storePrompt(t.id, t.template); + }); + }); + } + + onStop(): void { + } +} diff --git a/packages/ai-core/src/browser/ai-core-frontend-module.ts b/packages/ai-core/src/browser/ai-core-frontend-module.ts new file mode 100644 index 0000000000000..7f137f81ace05 --- /dev/null +++ b/packages/ai-core/src/browser/ai-core-frontend-module.ts @@ -0,0 +1,161 @@ +// ***************************************************************************** +// 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, CommandHandler } from '@theia/core'; +import { + RemoteConnectionProvider, + ServiceConnectionProvider, +} from '@theia/core/lib/browser/messaging/service-connection-provider'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { + AIVariableContribution, + AIVariableService, + ToolInvocationRegistry, + ToolInvocationRegistryImpl, + LanguageModelDelegateClient, + languageModelDelegatePath, + LanguageModelFrontendDelegate, + LanguageModelProvider, + LanguageModelRegistry, + LanguageModelRegistryClient, + languageModelRegistryDelegatePath, + LanguageModelRegistryFrontendDelegate, + PromptCustomizationService, + PromptService, + PromptServiceImpl, + ToolProvider +} from '../common'; +import { + FrontendLanguageModelRegistryImpl, + LanguageModelDelegateClientImpl, +} from './frontend-language-model-registry'; + +import { bindViewContribution, FrontendApplicationContribution, WidgetFactory } from '@theia/core/lib/browser'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate'; +import { AIAgentConfigurationWidget } from './ai-configuration/agent-configuration-widget'; +import { AIConfigurationSelectionService } from './ai-configuration/ai-configuration-service'; +import { AIAgentConfigurationViewContribution } from './ai-configuration/ai-configuration-view-contribution'; +import { AIConfigurationContainerWidget } from './ai-configuration/ai-configuration-widget'; +import { AIVariableConfigurationWidget } from './ai-configuration/variable-configuration-widget'; +import { AICoreFrontendApplicationContribution } from './ai-core-frontend-application-contribution'; +import { bindAICorePreferences } from './ai-core-preferences'; +import { AISettingsServiceImpl } from './ai-settings-service'; +import { FrontendPromptCustomizationServiceImpl } from './frontend-prompt-customization-service'; +import { FrontendVariableService } from './frontend-variable-service'; +import { PromptTemplateContribution } from './prompttemplate-contribution'; +import { TomorrowVariableContribution } from '../common/tomorrow-variable-contribution'; +import { TheiaVariableContribution } from './theia-variable-contribution'; +import { TodayVariableContribution } from '../common/today-variable-contribution'; +import { AgentsVariableContribution } from '../common/agents-variable-contribution'; +import { AIActivationService } from './ai-activation-service'; +import { AgentService, AgentServiceImpl } from '../common/agent-service'; +import { AICommandHandlerFactory } from './ai-command-handler-factory'; +import { AISettingsService } from '../common/settings-service'; + +export default new ContainerModule(bind => { + bindContributionProvider(bind, LanguageModelProvider); + + bind(FrontendLanguageModelRegistryImpl).toSelf().inSingletonScope(); + bind(LanguageModelRegistry).toService(FrontendLanguageModelRegistryImpl); + + bind(LanguageModelDelegateClientImpl).toSelf().inSingletonScope(); + bind(LanguageModelDelegateClient).toService(LanguageModelDelegateClientImpl); + bind(LanguageModelRegistryClient).toService(LanguageModelDelegateClient); + + bind(LanguageModelRegistryFrontendDelegate).toDynamicValue( + ctx => { + const connection = ctx.container.get(RemoteConnectionProvider); + const client = ctx.container.get(LanguageModelRegistryClient); + return connection.createProxy(languageModelRegistryDelegatePath, client); + } + ); + + bind(LanguageModelFrontendDelegate) + .toDynamicValue(ctx => { + const connection = ctx.container.get(RemoteConnectionProvider); + const client = ctx.container.get(LanguageModelDelegateClient); + return connection.createProxy(languageModelDelegatePath, client); + }) + .inSingletonScope(); + + bindAICorePreferences(bind); + + bind(FrontendPromptCustomizationServiceImpl).toSelf().inSingletonScope(); + bind(PromptCustomizationService).toService(FrontendPromptCustomizationServiceImpl); + bind(PromptServiceImpl).toSelf().inSingletonScope(); + bind(PromptService).toService(PromptServiceImpl); + + bind(PromptTemplateContribution).toSelf().inSingletonScope(); + bind(LanguageGrammarDefinitionContribution).toService(PromptTemplateContribution); + bind(CommandContribution).toService(PromptTemplateContribution); + bind(TabBarToolbarContribution).toService(PromptTemplateContribution); + + bind(AIConfigurationSelectionService).toSelf().inSingletonScope(); + bind(AIConfigurationContainerWidget).toSelf(); + bind(WidgetFactory) + .toDynamicValue(ctx => ({ + id: AIConfigurationContainerWidget.ID, + createWidget: () => ctx.container.get(AIConfigurationContainerWidget) + })) + .inSingletonScope(); + + bindViewContribution(bind, AIAgentConfigurationViewContribution); + bind(AISettingsService).to(AISettingsServiceImpl).inRequestScope(); + bindContributionProvider(bind, AIVariableContribution); + bind(FrontendVariableService).toSelf().inSingletonScope(); + bind(AIVariableService).toService(FrontendVariableService); + bind(FrontendApplicationContribution).toService(FrontendVariableService); + bind(AIVariableContribution).to(TheiaVariableContribution).inSingletonScope(); + bind(AIVariableContribution).to(TodayVariableContribution).inSingletonScope(); + bind(AIVariableContribution).to(TomorrowVariableContribution).inSingletonScope(); + bind(AIVariableContribution).to(AgentsVariableContribution).inSingletonScope(); + + bind(FrontendApplicationContribution).to(AICoreFrontendApplicationContribution).inSingletonScope(); + + bind(AIVariableConfigurationWidget).toSelf(); + bind(WidgetFactory) + .toDynamicValue(ctx => ({ + id: AIVariableConfigurationWidget.ID, + createWidget: () => ctx.container.get(AIVariableConfigurationWidget) + })) + .inSingletonScope(); + + bind(AIAgentConfigurationWidget).toSelf(); + bind(WidgetFactory) + .toDynamicValue(ctx => ({ + id: AIAgentConfigurationWidget.ID, + createWidget: () => ctx.container.get(AIAgentConfigurationWidget) + })) + .inSingletonScope(); + + bind(ToolInvocationRegistry).to(ToolInvocationRegistryImpl).inSingletonScope(); + bindContributionProvider(bind, ToolProvider); + + bind(AIActivationService).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(AIActivationService); + bind(AgentServiceImpl).toSelf().inSingletonScope(); + bind(AgentService).toService(AgentServiceImpl); + + bind(AICommandHandlerFactory).toFactory(context => (handler: CommandHandler) => { + const activationService = context.container.get(AIActivationService); + return { + execute: (...args: unknown[]) => handler.execute(...args), + isEnabled: (...args: unknown[]) => activationService.isActive && (handler.isEnabled?.(...args) ?? true), + isVisible: (...args: unknown[]) => activationService.isActive && (handler.isVisible?.(...args) ?? true), + isToggled: (...args: unknown[]) => handler.isToggled?.(...args) ?? false + }; + }); +}); diff --git a/packages/ai-core/src/browser/ai-core-preferences.ts b/packages/ai-core/src/browser/ai-core-preferences.ts new file mode 100644 index 0000000000000..970b1c485368f --- /dev/null +++ b/packages/ai-core/src/browser/ai-core-preferences.ts @@ -0,0 +1,76 @@ +// ***************************************************************************** +// 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 { PreferenceContribution, PreferenceProxy, PreferenceSchema } from '@theia/core/lib/browser'; +import { PreferenceProxyFactory } from '@theia/core/lib/browser/preferences/injectable-preference-proxy'; +import { interfaces } from '@theia/core/shared/inversify'; + +export const AI_CORE_PREFERENCES_TITLE = '✨ AI Features [Experimental]'; +export const PREFERENCE_NAME_ENABLE_EXPERIMENTAL = 'ai-features.AiEnable.enableAI'; +export const PREFERENCE_NAME_PROMPT_TEMPLATES = 'ai-features.promptTemplates.promptTemplatesFolder'; + +export const aiCorePreferenceSchema: PreferenceSchema = { + type: 'object', + properties: { + [PREFERENCE_NAME_ENABLE_EXPERIMENTAL]: { + title: AI_CORE_PREFERENCES_TITLE, + markdownDescription: '❗ This setting allows you to access and experiment with the latest AI capabilities.\ + \n\ + Please note that these features are in an experimental phase, which means they may be unstable and\ + undergo significant changes. It is important to be aware that these experimental features may generate\ + continuous requests to the language models (LLMs) you provide access to. This might incur costs that you\ + need to monitor closely. By enabling this option, you acknowledge these risks.\ + \n\ + **Please note! The settings below in this section will only take effect\n\ + once the main feature setting is enabled. After enabling the feature, you need to configure at least one\ + LLM provider below. Also see [the documentation](https://theia-ide.org/docs/user_ai/)**.', + type: 'boolean', + default: false, + }, + [PREFERENCE_NAME_PROMPT_TEMPLATES]: { + title: AI_CORE_PREFERENCES_TITLE, + description: 'Folder for storing customized prompt templates. If not customized the user config directory is used. Please consider to use a folder, which is\ + under version control to manage your variants of prompt templates.', + type: 'string', + default: '', + typeDetails: { + isFilepath: true, + selectionProps: { + openLabel: 'Select Folder', + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false + } + }, + + } + } +}; +export interface AICoreConfiguration { + [PREFERENCE_NAME_ENABLE_EXPERIMENTAL]: boolean | undefined; + [PREFERENCE_NAME_PROMPT_TEMPLATES]: string | undefined; +} + +export const AICorePreferences = Symbol('AICorePreferences'); +export type AICorePreferences = PreferenceProxy; + +export function bindAICorePreferences(bind: interfaces.Bind): void { + bind(AICorePreferences).toDynamicValue(ctx => { + const factory = ctx.container.get(PreferenceProxyFactory); + return factory(aiCorePreferenceSchema); + }).inSingletonScope(); + bind(PreferenceContribution).toConstantValue({ schema: aiCorePreferenceSchema }); +} diff --git a/packages/ai-core/src/browser/ai-settings-service.ts b/packages/ai-core/src/browser/ai-settings-service.ts new file mode 100644 index 0000000000000..f4c2ea5124688 --- /dev/null +++ b/packages/ai-core/src/browser/ai-settings-service.ts @@ -0,0 +1,50 @@ +// ***************************************************************************** +// 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 { DisposableCollection, Emitter, Event } from '@theia/core'; +import { PreferenceScope, PreferenceService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { JSONObject } from '@theia/core/shared/@phosphor/coreutils'; +import { AISettings, AISettingsService, AgentSettings } from '../common'; + +@injectable() +export class AISettingsServiceImpl implements AISettingsService { + @inject(PreferenceService) protected preferenceService: PreferenceService; + static readonly PREFERENCE_NAME = 'ai-features.agentSettings'; + + protected toDispose = new DisposableCollection(); + + protected readonly onDidChangeEmitter = new Emitter(); + onDidChange: Event = this.onDidChangeEmitter.event; + + async updateAgentSettings(agent: string, agentSettings: Partial): Promise { + const settings = await this.getSettings(); + const newAgentSettings = { ...settings[agent], ...agentSettings }; + settings[agent] = newAgentSettings; + this.preferenceService.set(AISettingsServiceImpl.PREFERENCE_NAME, settings, PreferenceScope.User); + this.onDidChangeEmitter.fire(); + } + + async getAgentSettings(agent: string): Promise { + const settings = await this.getSettings(); + return settings[agent]; + } + + async getSettings(): Promise { + await this.preferenceService.ready; + const pref = this.preferenceService.inspect(AISettingsServiceImpl.PREFERENCE_NAME); + return pref?.value ? pref.value : {}; + } +} diff --git a/packages/ai-core/src/browser/ai-view-contribution.ts b/packages/ai-core/src/browser/ai-view-contribution.ts new file mode 100644 index 0000000000000..9d43b455b5e13 --- /dev/null +++ b/packages/ai-core/src/browser/ai-view-contribution.ts @@ -0,0 +1,77 @@ +// ***************************************************************************** +// 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 { CommandRegistry, MenuModelRegistry } from '@theia/core'; +import { AbstractViewContribution, CommonMenus, KeybindingRegistry, PreferenceService, Widget } from '@theia/core/lib/browser'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { AIActivationService, EXPERIMENTAL_AI_CONTEXT_KEY } from './ai-activation-service'; +import { AICommandHandlerFactory } from './ai-command-handler-factory'; + +@injectable() +export class AIViewContribution extends AbstractViewContribution { + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + @inject(AIActivationService) + protected readonly activationService: AIActivationService; + + @inject(AICommandHandlerFactory) + protected readonly commandHandlerFactory: AICommandHandlerFactory; + + @postConstruct() + protected init(): void { + this.activationService.onDidChangeActiveStatus(active => { + if (!active) { + this.closeView(); + } + }); + } + + override registerCommands(commands: CommandRegistry): void { + if (this.toggleCommand) { + + commands.registerCommand(this.toggleCommand, this.commandHandlerFactory({ + execute: () => this.toggleView(), + })); + } + this.quickView?.registerItem({ + label: this.viewLabel, + when: EXPERIMENTAL_AI_CONTEXT_KEY, + open: () => this.openView({ activate: true }) + }); + + } + + override registerMenus(menus: MenuModelRegistry): void { + if (this.toggleCommand) { + menus.registerMenuAction(CommonMenus.VIEW_VIEWS, { + commandId: this.toggleCommand.id, + when: EXPERIMENTAL_AI_CONTEXT_KEY, + label: this.viewLabel + }); + } + } + override registerKeybindings(keybindings: KeybindingRegistry): void { + if (this.toggleCommand && this.options.toggleKeybinding) { + keybindings.registerKeybinding({ + command: this.toggleCommand.id, + when: EXPERIMENTAL_AI_CONTEXT_KEY, + keybinding: this.options.toggleKeybinding + }); + } + } +} + diff --git a/packages/ai-core/src/browser/frontend-language-model-registry.ts b/packages/ai-core/src/browser/frontend-language-model-registry.ts new file mode 100644 index 0000000000000..90b80a0688451 --- /dev/null +++ b/packages/ai-core/src/browser/frontend-language-model-registry.ts @@ -0,0 +1,405 @@ +// ***************************************************************************** +// 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, ILogger } from '@theia/core'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; +import { + OutputChannel, + OutputChannelManager, + OutputChannelSeverity, +} from '@theia/output/lib/browser/output-channel'; +import { + AISettingsService, + DefaultLanguageModelRegistryImpl, + isLanguageModelParsedResponse, + isLanguageModelStreamResponse, + isLanguageModelStreamResponseDelegate, + isLanguageModelTextResponse, + isModelMatching, + LanguageModel, + LanguageModelDelegateClient, + LanguageModelFrontendDelegate, + LanguageModelMetaData, + LanguageModelRegistryClient, + LanguageModelRegistryFrontendDelegate, + LanguageModelRequest, + LanguageModelResponse, + LanguageModelSelector, + LanguageModelStreamResponsePart, +} from '../common'; + +@injectable() +export class LanguageModelDelegateClientImpl + implements LanguageModelDelegateClient, LanguageModelRegistryClient { + protected receiver: FrontendLanguageModelRegistryImpl; + + setReceiver(receiver: FrontendLanguageModelRegistryImpl): void { + this.receiver = receiver; + } + + send(id: string, token: LanguageModelStreamResponsePart | undefined): void { + this.receiver.send(id, token); + } + + toolCall(requestId: string, toolId: string, args_string: string): Promise { + return this.receiver.toolCall(requestId, toolId, args_string); + } + + languageModelAdded(metadata: LanguageModelMetaData): void { + this.receiver.languageModelAdded(metadata); + } + + languageModelRemoved(id: string): void { + this.receiver.languageModelRemoved(id); + } +} + +interface StreamState { + id: string; + tokens: (LanguageModelStreamResponsePart | undefined)[]; + resolve?: (_: unknown) => void; +} + +@injectable() +export class FrontendLanguageModelRegistryImpl + extends DefaultLanguageModelRegistryImpl { + + // called by backend + languageModelAdded(metadata: LanguageModelMetaData): void { + this.addLanguageModels([metadata]); + } + // called by backend + languageModelRemoved(id: string): void { + this.removeLanguageModels([id]); + } + @inject(LanguageModelRegistryFrontendDelegate) + protected registryDelegate: LanguageModelRegistryFrontendDelegate; + + @inject(LanguageModelFrontendDelegate) + protected providerDelegate: LanguageModelFrontendDelegate; + + @inject(LanguageModelDelegateClientImpl) + protected client: LanguageModelDelegateClientImpl; + + @inject(ILogger) + protected override logger: ILogger; + + @inject(OutputChannelManager) + protected outputChannelManager: OutputChannelManager; + + @inject(AISettingsService) + protected settingsService: AISettingsService; + + private static requestCounter: number = 0; + + override addLanguageModels(models: LanguageModelMetaData[] | LanguageModel[]): void { + let modelAdded = false; + for (const model of models) { + if (this.languageModels.find(m => m.id === model.id)) { + console.warn(`Tried to add an existing model ${model.id}`); + continue; + } + if (LanguageModel.is(model)) { + this.languageModels.push( + new Proxy( + model, + languageModelOutputHandler( + () => this.outputChannelManager.getChannel( + model.id + ) + ) + ) + ); + modelAdded = true; + } else { + this.languageModels.push( + new Proxy( + this.createFrontendLanguageModel( + model + ), + languageModelOutputHandler( + () => this.outputChannelManager.getChannel( + model.id + ) + ) + ) + ); + modelAdded = true; + } + } + if (modelAdded) { + this.changeEmitter.fire({ models: this.languageModels }); + } + } + + @postConstruct() + protected override init(): void { + this.client.setReceiver(this); + + const contributions = + this.languageModelContributions.getContributions(); + const promises = contributions.map(provider => provider()); + const backendDescriptions = + this.registryDelegate.getLanguageModelDescriptions(); + + Promise.allSettled([backendDescriptions, ...promises]).then( + results => { + const backendDescriptionsResult = results[0]; + if (backendDescriptionsResult.status === 'fulfilled') { + this.addLanguageModels(backendDescriptionsResult.value); + } else { + this.logger.error( + 'Failed to add language models contributed from the backend', + backendDescriptionsResult.reason + ); + } + for (let i = 1; i < results.length; i++) { + // assert that index > 0 contains only language models + const languageModelResult = results[i] as + | PromiseRejectedResult + | PromiseFulfilledResult; + if (languageModelResult.status === 'fulfilled') { + this.addLanguageModels(languageModelResult.value); + } else { + this.logger.error( + 'Failed to add some language models:', + languageModelResult.reason + ); + } + } + this.markInitialized(); + } + ); + } + + createFrontendLanguageModel( + description: LanguageModelMetaData + ): LanguageModel { + return { + ...description, + request: async (request: LanguageModelRequest, cancellationToken?: CancellationToken) => { + const requestId = `${FrontendLanguageModelRegistryImpl.requestCounter++}`; + this.requests.set(requestId, request); + cancellationToken?.onCancellationRequested(() => { + this.providerDelegate.cancel(requestId); + }); + const response = await this.providerDelegate.request( + description.id, + request, + requestId, + cancellationToken + ); + if (isLanguageModelTextResponse(response) || isLanguageModelParsedResponse(response)) { + return response; + } + if (isLanguageModelStreamResponseDelegate(response)) { + if (!this.streams.has(response.streamId)) { + const newStreamState = { + id: response.streamId, + tokens: [], + }; + this.streams.set(response.streamId, newStreamState); + } + const streamState = this.streams.get(response.streamId)!; + return { + stream: this.getIterable(streamState), + }; + } + this.logger.error( + `Received unknown response in frontend for request to language model ${description.id}. Trying to continue without touching the response.`, + response + ); + return response; + }, + }; + } + + private streams = new Map(); + private requests = new Map(); + + async *getIterable( + state: StreamState + ): AsyncIterable { + let current = -1; + while (true) { + if (current < state.tokens.length - 1) { + current++; + const token = state.tokens[current]; + if (token === undefined) { + // message is finished + break; + } + if (token !== undefined) { + yield token; + } + } else { + await new Promise(resolve => { + state.resolve = resolve; + }); + } + } + this.streams.delete(state.id); + } + + // called by backend via the "delegate client" with new tokens + send(id: string, token: LanguageModelStreamResponsePart | undefined): void { + if (!this.streams.has(id)) { + const newStreamState = { + id, + tokens: [], + }; + this.streams.set(id, newStreamState); + } + const streamState = this.streams.get(id)!; + streamState.tokens.push(token); + if (streamState.resolve) { + streamState.resolve(token); + } + } + + // called by backend once tool is invoked + toolCall(id: string, toolId: string, arg_string: string): Promise { + if (!this.requests.has(id)) { + throw new Error('Somehow we got a callback for a non existing request!'); + } + const request = this.requests.get(id)!; + const tool = request.tools?.find(t => t.id === toolId); + if (tool) { + return tool.handler(arg_string); + } + throw new Error(`Could not find a tool for ${toolId}!`); + } + + override async selectLanguageModels(request: LanguageModelSelector): Promise { + await this.initialized; + const userSettings = (await this.settingsService.getAgentSettings(request.agent))?.languageModelRequirements?.find(req => req.purpose === request.purpose); + if (userSettings?.identifier) { + const model = await this.getLanguageModel(userSettings.identifier); + if (model) { + return [model]; + } + } + return this.languageModels.filter(model => isModelMatching(request, model)); + } + + override async selectLanguageModel(request: LanguageModelSelector): Promise { + return (await this.selectLanguageModels(request))[0]; + } +} + +const formatJsonWithIndentation = (obj: unknown): string[] => { + // eslint-disable-next-line no-null/no-null + const jsonString = JSON.stringify(obj, null, 2); + const lines = jsonString.split('\n'); + const formattedLines: string[] = []; + + lines.forEach(line => { + const subLines = line.split('\\n'); + const index = indexOfValue(subLines[0]) + 1; + formattedLines.push(subLines[0]); + const prefix = index > 0 ? ' '.repeat(index) : ''; + if (index !== -1) { + for (let i = 1; i < subLines.length; i++) { + formattedLines.push(prefix + subLines[i]); + } + } + }); + + return formattedLines; +}; + +const indexOfValue = (jsonLine: string): number => { + const pattern = /"([^"]+)"\s*:\s*/g; + const match = pattern.exec(jsonLine); + return match ? match.index + match[0].length : -1; +}; + +const languageModelOutputHandler = ( + outputChannelGetter: () => OutputChannel +): ProxyHandler => ({ + get( + target: LanguageModel, + prop: K, + ): LanguageModel[K] | LanguageModel['request'] { + const original = target[prop]; + if (prop === 'request' && typeof original === 'function') { + return async function ( + ...args: Parameters + ): Promise { + const outputChannel = outputChannelGetter(); + outputChannel.appendLine( + 'Sending request:' + ); + const formattedRequest = formatJsonWithIndentation(args[0]); + formattedRequest.forEach(line => outputChannel.appendLine(line)); + if (args[1]) { + args[1] = new Proxy(args[1], { + get( + cTarget: CancellationToken, + cProp: CK + ): CancellationToken[CK] | CancellationToken['onCancellationRequested'] { + if (cProp === 'onCancellationRequested') { + return (...cargs: Parameters) => cTarget.onCancellationRequested(() => { + outputChannel.appendLine('\nCancel requested', OutputChannelSeverity.Warning); + cargs[0](); + }, cargs[1], cargs[2]); + } + return cTarget[cProp]; + } + }); + } + try { + const result = await original.apply(target, args); + if (isLanguageModelStreamResponse(result)) { + outputChannel.appendLine('Received a response stream'); + const stream = result.stream; + const loggedStream = { + async *[Symbol.asyncIterator](): AsyncIterator { + for await (const part of stream) { + outputChannel.append(part.content || ''); + yield part; + } + outputChannel.append('\n'); + outputChannel.appendLine('End of stream'); + }, + }; + return { + ...result, + stream: loggedStream, + }; + } else { + outputChannel.appendLine('Received a response'); + outputChannel.appendLine(JSON.stringify(result)); + return result; + } + } catch (err) { + outputChannel.appendLine('An error occurred'); + if (err instanceof Error) { + outputChannel.appendLine( + err.message, + OutputChannelSeverity.Error + ); + } + throw err; + } + }; + } + return original; + }, +}); diff --git a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts new file mode 100644 index 0000000000000..11be74b482834 --- /dev/null +++ b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts @@ -0,0 +1,256 @@ +// ***************************************************************************** +// 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 { DisposableCollection, URI, Event, Emitter } from '@theia/core'; +import { OpenerService } from '@theia/core/lib/browser'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { PromptCustomizationService, PromptTemplate, CustomAgentDescription } from '../common'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileChangesEvent } from '@theia/filesystem/lib/common/files'; +import { AICorePreferences, PREFERENCE_NAME_PROMPT_TEMPLATES } from './ai-core-preferences'; +import { AgentService } from '../common/agent-service'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { load, dump } from 'js-yaml'; + +const templateEntry = { + id: 'my_agent', + name: 'My Agent', + description: 'This is an example agent. Please adapt the properties to fit your needs.', + prompt: 'You are an example agent. Be nice and helpful to the user.', + defaultLLM: 'openai/gpt-4o' +}; + +@injectable() +export class FrontendPromptCustomizationServiceImpl implements PromptCustomizationService { + + @inject(EnvVariablesServer) + protected readonly envVariablesServer: EnvVariablesServer; + + @inject(AICorePreferences) + protected readonly preferences: AICorePreferences; + + @inject(FileService) + protected readonly fileService: FileService; + + @inject(OpenerService) + protected readonly openerService: OpenerService; + + @inject(AgentService) + protected readonly agentService: AgentService; + + protected readonly trackedTemplateURIs = new Set(); + protected templates = new Map(); + + protected toDispose = new DisposableCollection(); + + private readonly onDidChangePromptEmitter = new Emitter(); + readonly onDidChangePrompt: Event = this.onDidChangePromptEmitter.event; + + private readonly onDidChangeCustomAgentsEmitter = new Emitter(); + readonly onDidChangeCustomAgents = this.onDidChangeCustomAgentsEmitter.event; + + @postConstruct() + protected init(): void { + this.preferences.onPreferenceChanged(event => { + if (event.preferenceName === PREFERENCE_NAME_PROMPT_TEMPLATES) { + this.update(); + } + }); + this.update(); + } + + protected async update(): Promise { + this.toDispose.dispose(); + // we need to assign a local variable, so that updates running in parallel don't interfere with each other + const _templates = new Map(); + this.templates = _templates; + this.trackedTemplateURIs.clear(); + + const templateURI = await this.getTemplatesDirectoryURI(); + + this.toDispose.push(this.fileService.watch(templateURI, { recursive: true, excludes: [] })); + this.toDispose.push(this.fileService.onDidFilesChange(async (event: FileChangesEvent) => { + if (event.changes.some(change => change.resource.toString().endsWith('customAgents.yml'))) { + this.onDidChangeCustomAgentsEmitter.fire(); + } + // check deleted templates + for (const deletedFile of event.getDeleted()) { + if (this.trackedTemplateURIs.has(deletedFile.resource.toString())) { + this.trackedTemplateURIs.delete(deletedFile.resource.toString()); + const templateId = this.removePromptTemplateSuffix(deletedFile.resource.path.name); + _templates.delete(templateId); + this.onDidChangePromptEmitter.fire(templateId); + } + } + // check updated templates + for (const updatedFile of event.getUpdated()) { + if (this.trackedTemplateURIs.has(updatedFile.resource.toString())) { + const filecontent = await this.fileService.read(updatedFile.resource); + const templateId = this.removePromptTemplateSuffix(updatedFile.resource.path.name); + _templates.set(templateId, filecontent.value); + this.onDidChangePromptEmitter.fire(templateId); + } + } + // check new templates + for (const addedFile of event.getAdded()) { + if (addedFile.resource.parent.toString() === templateURI.toString() && addedFile.resource.path.ext === '.prompttemplate') { + this.trackedTemplateURIs.add(addedFile.resource.toString()); + const filecontent = await this.fileService.read(addedFile.resource); + const templateId = this.removePromptTemplateSuffix(addedFile.resource.path.name); + _templates.set(templateId, filecontent.value); + this.onDidChangePromptEmitter.fire(templateId); + } + } + + })); + + this.onDidChangeCustomAgentsEmitter.fire(); + const stat = await this.fileService.resolve(templateURI); + if (stat.children === undefined) { + return; + } + + for (const file of stat.children) { + if (!file.isFile) { + continue; + } + const fileURI = file.resource; + if (fileURI.path.ext === '.prompttemplate') { + this.trackedTemplateURIs.add(fileURI.toString()); + const filecontent = await this.fileService.read(fileURI); + const templateId = this.removePromptTemplateSuffix(file.name); + _templates.set(templateId, filecontent.value); + this.onDidChangePromptEmitter.fire(templateId); + } + } + } + + protected async getTemplatesDirectoryURI(): Promise { + const templatesFolder = this.preferences[PREFERENCE_NAME_PROMPT_TEMPLATES]; + if (templatesFolder && templatesFolder.trim().length > 0) { + return URI.fromFilePath(templatesFolder); + } + const theiaConfigDir = await this.envVariablesServer.getConfigDirUri(); + return new URI(theiaConfigDir).resolve('prompt-templates'); + } + + protected async getTemplateURI(templateId: string): Promise { + return (await this.getTemplatesDirectoryURI()).resolve(`${templateId}.prompttemplate`); + } + + protected removePromptTemplateSuffix(filename: string): string { + const suffix = '.prompttemplate'; + if (filename.endsWith(suffix)) { + return filename.slice(0, -suffix.length); + } + return filename; + } + + isPromptTemplateCustomized(id: string): boolean { + return this.templates.has(id); + } + + getCustomizedPromptTemplate(id: string): string | undefined { + return this.templates.get(id); + } + + async editTemplate(id: string, content?: string): Promise { + const template = this.getOriginalTemplate(id); + if (template === undefined) { + throw new Error(`Unable to edit template ${id}: template not found.`); + } + const editorUri = await this.getTemplateURI(id); + if (! await this.fileService.exists(editorUri)) { + await this.fileService.createFile(editorUri, BinaryBuffer.fromString(content ?? template.template)); + } else if (content) { + // Write content to the file before opening it + await this.fileService.writeFile(editorUri, BinaryBuffer.fromString(content)); + } + const openHandler = await this.openerService.getOpener(editorUri); + openHandler.open(editorUri); + } + + async resetTemplate(id: string): Promise { + const editorUri = await this.getTemplateURI(id); + if (await this.fileService.exists(editorUri)) { + await this.fileService.delete(editorUri); + } + } + + getOriginalTemplate(id: string): PromptTemplate | undefined { + for (const agent of this.agentService.getAllAgents()) { + for (const template of agent.promptTemplates) { + if (template.id === id) { + return template; + } + } + } + return undefined; + } + + getTemplateIDFromURI(uri: URI): string | undefined { + const id = this.removePromptTemplateSuffix(uri.path.name); + if (this.templates.has(id)) { + return id; + } + return undefined; + } + + async getCustomAgents(): Promise { + const customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml'); + const yamlExists = await this.fileService.exists(customAgentYamlUri); + if (!yamlExists) { + return []; + } + const filecontent = await this.fileService.read(customAgentYamlUri, { encoding: 'utf-8' }); + try { + const doc = load(filecontent.value); + if (!Array.isArray(doc) || !doc.every(entry => CustomAgentDescription.is(entry))) { + console.debug('Invalid customAgents.yml file content'); + return []; + } + const readAgents = doc as CustomAgentDescription[]; + // make sure all agents are unique (id and name) + const uniqueAgentIds = new Set(); + const uniqueAgens: CustomAgentDescription[] = []; + readAgents.forEach(agent => { + if (uniqueAgentIds.has(agent.id)) { + return; + } + uniqueAgentIds.add(agent.id); + uniqueAgens.push(agent); + }); + return uniqueAgens; + } catch (e) { + console.debug(e.message, e); + return []; + } + } + + async openCustomAgentYaml(): Promise { + const customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml'); + const content = dump([templateEntry]); + if (! await this.fileService.exists(customAgentYamlUri)) { + await this.fileService.createFile(customAgentYamlUri, BinaryBuffer.fromString(content)); + } else { + const fileContent = (await this.fileService.readFile(customAgentYamlUri)).value; + await this.fileService.writeFile(customAgentYamlUri, BinaryBuffer.concat([fileContent, BinaryBuffer.fromString(content)])); + } + const openHandler = await this.openerService.getOpener(customAgentYamlUri); + openHandler.open(customAgentYamlUri); + } +} diff --git a/packages/ai-core/src/browser/frontend-variable-service.ts b/packages/ai-core/src/browser/frontend-variable-service.ts new file mode 100644 index 0000000000000..56ceda7e4edd8 --- /dev/null +++ b/packages/ai-core/src/browser/frontend-variable-service.ts @@ -0,0 +1,26 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; +import { DefaultAIVariableService } from '../common'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; + +@injectable() +export class FrontendVariableService extends DefaultAIVariableService implements FrontendApplicationContribution { + onStart(): void { + this.initContributions(); + } +} diff --git a/packages/ai-core/src/browser/index.ts b/packages/ai-core/src/browser/index.ts new file mode 100644 index 0000000000000..443f3894e72f4 --- /dev/null +++ b/packages/ai-core/src/browser/index.ts @@ -0,0 +1,26 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export * from './ai-activation-service'; +export * from './ai-core-frontend-application-contribution'; +export * from './ai-core-frontend-module'; +export * from './ai-core-preferences'; +export * from './ai-settings-service'; +export * from './ai-view-contribution'; +export * from './frontend-language-model-registry'; +export * from './frontend-variable-service'; +export * from './prompttemplate-contribution'; +export * from './theia-variable-contribution'; diff --git a/packages/ai-core/src/browser/prompttemplate-contribution.ts b/packages/ai-core/src/browser/prompttemplate-contribution.ts new file mode 100644 index 0000000000000..d3cf4f99a6814 --- /dev/null +++ b/packages/ai-core/src/browser/prompttemplate-contribution.ts @@ -0,0 +1,250 @@ +// ***************************************************************************** +// 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, named } from '@theia/core/shared/inversify'; +import { GrammarDefinition, GrammarDefinitionProvider, LanguageGrammarDefinitionContribution, TextmateRegistry } from '@theia/monaco/lib/browser/textmate'; +import * as monaco from '@theia/monaco-editor-core'; +import { Command, CommandContribution, CommandRegistry, ContributionProvider, MessageService } from '@theia/core'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; + +import { codicon, Widget } from '@theia/core/lib/browser'; +import { EditorWidget, ReplaceOperation } from '@theia/editor/lib/browser'; +import { PromptCustomizationService, PromptService, ToolProvider } from '../common'; +import { ProviderResult } from '@theia/monaco-editor-core/esm/vs/editor/common/languages'; + +const PROMPT_TEMPLATE_LANGUAGE_ID = 'theia-ai-prompt-template'; +const PROMPT_TEMPLATE_TEXTMATE_SCOPE = 'source.prompttemplate'; + +export const DISCARD_PROMPT_TEMPLATE_CUSTOMIZATIONS: Command = { + id: 'theia-ai-prompt-template:discard', + iconClass: codicon('discard'), + category: 'Theia AI Prompt Templates' +}; + +// TODO this command is mainly for testing purposes +export const SHOW_ALL_PROMPTS_COMMAND: Command = { + id: 'theia-ai-prompt-template:show-prompts-command', + label: 'Show all prompts', + iconClass: codicon('beaker'), + category: 'Theia AI Prompt Templates', +}; + +@injectable() +export class PromptTemplateContribution implements LanguageGrammarDefinitionContribution, CommandContribution, TabBarToolbarContribution { + + @inject(PromptService) + private readonly promptService: PromptService; + + @inject(MessageService) + private readonly messageService: MessageService; + + @inject(PromptCustomizationService) + protected readonly customizationService: PromptCustomizationService; + + @inject(ContributionProvider) + @named(ToolProvider) + private toolProviders: ContributionProvider; + + readonly config: monaco.languages.LanguageConfiguration = + { + 'brackets': [ + ['${', '}'], + ['~{', '}'] + ], + 'autoClosingPairs': [ + { 'open': '${', 'close': '}' }, + { 'open': '~{', 'close': '}' }, + ], + 'surroundingPairs': [ + { 'open': '${', 'close': '}' }, + { 'open': '~{', 'close': '}' } + ] + }; + + registerTextmateLanguage(registry: TextmateRegistry): void { + monaco.languages.register({ + id: PROMPT_TEMPLATE_LANGUAGE_ID, + 'aliases': [ + 'Theia AI Prompt Templates' + ], + 'extensions': [ + '.prompttemplate', + ], + 'filenames': [] + }); + + monaco.languages.setLanguageConfiguration(PROMPT_TEMPLATE_LANGUAGE_ID, this.config); + + monaco.languages.registerCompletionItemProvider(PROMPT_TEMPLATE_LANGUAGE_ID, { + // Monaco only supports single character trigger characters + triggerCharacters: ['{'], + provideCompletionItems: (model, position, _context, _token): ProviderResult => this.provideFunctionCompletions(model, position), + }); + + const textmateGrammar = require('../../data/prompttemplate.tmLanguage.json'); + const grammarDefinitionProvider: GrammarDefinitionProvider = { + getGrammarDefinition: function (): Promise { + return Promise.resolve({ + format: 'json', + content: textmateGrammar + }); + } + }; + registry.registerTextmateGrammarScope(PROMPT_TEMPLATE_TEXTMATE_SCOPE, grammarDefinitionProvider); + + registry.mapLanguageIdToTextmateGrammar(PROMPT_TEMPLATE_LANGUAGE_ID, PROMPT_TEMPLATE_TEXTMATE_SCOPE); + } + + provideFunctionCompletions(model: monaco.editor.ITextModel, position: monaco.Position): ProviderResult { + return this.getSuggestions( + model, + position, + '~{', + this.toolProviders.getContributions().map(provider => provider.getTool()), + monaco.languages.CompletionItemKind.Function, + tool => tool.id, + tool => tool.name, + tool => tool.description ?? '' + ); + } + + getCompletionRange(model: monaco.editor.ITextModel, position: monaco.Position, triggerCharacters: string): monaco.Range | undefined { + // Check if the characters before the current position are the trigger characters + const lineContent = model.getLineContent(position.lineNumber); + const triggerLength = triggerCharacters.length; + const charactersBefore = lineContent.substring( + position.column - triggerLength - 1, + position.column - 1 + ); + + if (charactersBefore !== triggerCharacters) { + // Do not return agent suggestions if the user didn't just type the trigger characters + return undefined; + } + + // Calculate the range from the position of the trigger characters + const wordInfo = model.getWordUntilPosition(position); + return new monaco.Range( + position.lineNumber, + wordInfo.startColumn, + position.lineNumber, + position.column + ); + } + + private getSuggestions( + model: monaco.editor.ITextModel, + position: monaco.Position, + triggerChars: string, + items: T[], + kind: monaco.languages.CompletionItemKind, + getId: (item: T) => string, + getName: (item: T) => string, + getDescription: (item: T) => string + ): ProviderResult { + const completionRange = this.getCompletionRange(model, position, triggerChars); + if (completionRange === undefined) { + return { suggestions: [] }; + } + const suggestions = items.map(item => ({ + insertText: getId(item), + kind: kind, + label: getName(item), + range: completionRange, + detail: getDescription(item), + })); + return { suggestions }; + } + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(DISCARD_PROMPT_TEMPLATE_CUSTOMIZATIONS, { + isVisible: (widget: Widget) => this.isPromptTemplateWidget(widget), + isEnabled: (widget: EditorWidget) => this.canDiscard(widget), + execute: (widget: EditorWidget) => this.discard(widget) + }); + + commands.registerCommand(SHOW_ALL_PROMPTS_COMMAND, { + execute: () => this.showAllPrompts() + }); + } + + protected isPromptTemplateWidget(widget: Widget): boolean { + if (widget instanceof EditorWidget) { + return PROMPT_TEMPLATE_LANGUAGE_ID === widget.editor.document.languageId; + } + return false; + } + + protected canDiscard(widget: EditorWidget): boolean { + const resourceUri = widget.editor.uri; + const id = this.customizationService.getTemplateIDFromURI(resourceUri); + if (id === undefined) { + return false; + } + const rawPrompt = this.promptService.getRawPrompt(id); + const defaultPrompt = this.promptService.getDefaultRawPrompt(id); + return rawPrompt?.template !== defaultPrompt?.template; + } + + protected async discard(widget: EditorWidget): Promise { + const resourceUri = widget.editor.uri; + const id = this.customizationService.getTemplateIDFromURI(resourceUri); + if (id === undefined) { + return; + } + const defaultPrompt = this.promptService.getDefaultRawPrompt(id); + if (defaultPrompt === undefined) { + return; + } + + const source: string = widget.editor.document.getText(); + const lastLine = widget.editor.document.getLineContent(widget.editor.document.lineCount); + + const replaceOperation: ReplaceOperation = { + range: { + start: { + line: 0, + character: 0 + }, + end: { + line: widget.editor.document.lineCount, + character: lastLine.length + } + }, + text: defaultPrompt.template + }; + + await widget.editor.replaceText({ + source, + replaceOperations: [replaceOperation] + }); + } + + private showAllPrompts(): void { + const allPrompts = this.promptService.getAllPrompts(); + Object.keys(allPrompts).forEach(id => { + this.messageService.info(`Prompt Template ID: ${id}\n${allPrompts[id].template}`, 'Got it'); + }); + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: DISCARD_PROMPT_TEMPLATE_CUSTOMIZATIONS.id, + command: DISCARD_PROMPT_TEMPLATE_CUSTOMIZATIONS.id, + tooltip: 'Discard Customizations' + }); + } +} diff --git a/packages/ai-core/src/browser/style/index.css b/packages/ai-core/src/browser/style/index.css new file mode 100644 index 0000000000000..36cdad9c19221 --- /dev/null +++ b/packages/ai-core/src/browser/style/index.css @@ -0,0 +1,95 @@ +.ai-configuration-widget { + padding: var(--theia-ui-padding); +} + +.theia-ai-settings-container { + padding: var(--theia-ui-padding); +} + +.language-model-container { + padding-top: calc(2 * var(--theia-ui-padding)); +} + +.language-model-container .theia-select { + margin-left: var(--theia-ui-padding); +} + +.ai-templates { + display: grid; + /** Display content in 3 columns */ + grid-template-columns: 1fr auto auto; + /** add a 3px gap between rows */ + row-gap: 3px; +} + +#ai-variable-configuration-container-widget, +#ai-agent-configuration-container-widget { + margin-top: 5px; +} + +/* Variable Settings */ +#ai-variable-configuration-container-widget ul { + list-style: none; + padding: 0; + margin: 0; +} + +#ai-variable-configuration-container-widget .variable-item { + display: flex; + flex-direction: column; + margin-bottom: 1rem; +} + +#ai-variable-configuration-container-widget .variable-args { + display: grid; + grid-template-columns: 1fr 2fr; +} + +/* Agent Settings */ +#ai-agent-configuration-container-widget ul { + list-style: none; + padding: 0; + margin: 0; +} + +.ai-agent-configuration-main { + display: flex; + flex-direction: row; +} + +.configuration-agents-list { + width: 128px; +} + +.configuration-agent-panel { + flex: 1; +} + +#ai-variable-configuration-container-widget .variable-references, +#ai-agent-configuration-container-widget .variable-references, +#ai-agent-configuration-container-widget .function-references { + margin-left: 0.5rem; + padding: 0.5rem; + border-left: solid 1px var(--theia-tree-indentGuidesStroke); +} + +#ai-variable-configuration-container-widget .variable-reference, +#ai-agent-configuration-container-widget .variable-reference, +#ai-agent-configuration-container-widget .function-reference { + display: flex; + flex-direction: row; + align-items: center; +} + +.agent-tag { + padding: calc(var(--theia-ui-padding) * 2 / 3); + padding-top: 0px; + padding-bottom: 0px; + border-radius: calc(var(--theia-ui-padding) * 2 / 3); + background: hsla(0, 0%, 68%, 0.31); +} + +.configuration-agents-add { + margin-top: 3em; + margin-left: 0; +} diff --git a/packages/ai-core/src/browser/theia-variable-contribution.ts b/packages/ai-core/src/browser/theia-variable-contribution.ts new file mode 100644 index 0000000000000..f8353e5eecb00 --- /dev/null +++ b/packages/ai-core/src/browser/theia-variable-contribution.ts @@ -0,0 +1,58 @@ +// ***************************************************************************** +// 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 { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { VariableRegistry, VariableResolverService } from '@theia/variable-resolver/lib/browser'; +import { AIVariableContribution, AIVariableResolver, AIVariableService, AIVariableResolutionRequest, AIVariableContext, ResolvedAIVariable } from '../common'; + +@injectable() +export class TheiaVariableContribution implements AIVariableContribution, AIVariableResolver { + @inject(VariableResolverService) + protected readonly variableResolverService: VariableResolverService; + + @inject(VariableRegistry) + protected readonly variableRegistry: VariableRegistry; + + @inject(FrontendApplicationStateService) + protected readonly stateService: FrontendApplicationStateService; + + registerVariables(service: AIVariableService): void { + this.stateService.reachedState('initialized_layout').then(() => { + // some variable contributions in Theia are done as part of the onStart, same as our AI variable contributions + // we therefore wait for all of them to be registered before we register we map them to our own + this.variableRegistry.getVariables().forEach(variable => { + service.registerResolver({ id: `theia-${variable.name}`, name: variable.name, description: variable.description ?? 'Theia Built-in Variable' }, this); + }); + }); + } + + protected toTheiaVariable(request: AIVariableResolutionRequest): string { + return `$\{${request.variable.name}${request.arg ? ':' + request.arg : ''}}`; + } + + async canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise { + // some variables are not resolvable without providing a specific context + // this may be expensive but was not a problem for Theia's built-in variables + const resolved = await this.variableResolverService.resolve(this.toTheiaVariable(request), context); + return !resolved ? 0 : 1; + } + + async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise { + const resolved = await this.variableResolverService.resolve(this.toTheiaVariable(request), context); + return resolved ? { value: resolved, variable: request.variable } : undefined; + } +} + diff --git a/packages/ai-core/src/common/agent-service.ts b/packages/ai-core/src/common/agent-service.ts new file mode 100644 index 0000000000000..7bb5b0f01a57a --- /dev/null +++ b/packages/ai-core/src/common/agent-service.ts @@ -0,0 +1,135 @@ +// ***************************************************************************** +// 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, named, optional, postConstruct } from '@theia/core/shared/inversify'; +import { ContributionProvider, Emitter, Event } from '@theia/core'; +import { Agent } from './agent'; +import { AISettingsService } from './settings-service'; + +export const AgentService = Symbol('AgentService'); + +/** + * Service to access the list of known Agents. + */ +export interface AgentService { + /** + * Retrieves a list of all available agents, i.e. agents which are not disabled + */ + getAgents(): Agent[]; + /** + * Retrieves a list of all agents, including disabled ones. + */ + getAllAgents(): Agent[]; + /** + * Enable the agent with the specified id. + * @param agentId the agent id. + */ + enableAgent(agentId: string): void; + /** + * disable the agent with the specified id. + * @param agentId the agent id. + */ + disableAgent(agentId: string): void; + /** + * query whether this agent is currently enabled or disabled. + * @param agentId the agent id. + * @return true if the agent is enabled, false otherwise. + */ + isEnabled(agentId: string): boolean; + + /** + * Allows to register an agent programmatically. + * @param agent the agent to register + */ + registerAgent(agent: Agent): void; + + /** + * Allows to unregister an agent programmatically. + * @param agentId the agent id to unregister + */ + unregisterAgent(agentId: string): void; + + /** + * Emitted when the list of agents changes. + * This can be used to update the UI when agents are added or removed. + */ + onDidChangeAgents: Event; +} + +@injectable() +export class AgentServiceImpl implements AgentService { + + @inject(ContributionProvider) @named(Agent) + protected readonly agentsProvider: ContributionProvider; + + @inject(AISettingsService) @optional() + protected readonly aiSettingsService: AISettingsService | undefined; + + protected disabledAgents = new Set(); + + protected _agents: Agent[] = []; + + private readonly onDidChangeAgentsEmitter = new Emitter(); + readonly onDidChangeAgents = this.onDidChangeAgentsEmitter.event; + + @postConstruct() + protected init(): void { + this.aiSettingsService?.getSettings().then(settings => { + Object.entries(settings).forEach(([agentId, agentSettings]) => { + if (agentSettings.enable === false) { + this.disabledAgents.add(agentId); + } + }); + }); + } + + private get agents(): Agent[] { + // We can't collect the contributions at @postConstruct because this will lead to a circular dependency + // with agents reusing the chat agent service (e.g. orchestrator) which in turn injects the agent service + return [...this.agentsProvider.getContributions(), ...this._agents]; + } + + registerAgent(agent: Agent): void { + this._agents.push(agent); + this.onDidChangeAgentsEmitter.fire(); + } + + unregisterAgent(agentId: string): void { + this._agents = this._agents.filter(a => a.id !== agentId); + this.onDidChangeAgentsEmitter.fire(); + } + + getAgents(): Agent[] { + return this.agents.filter(agent => this.isEnabled(agent.id)); + } + + getAllAgents(): Agent[] { + return this.agents; + } + + enableAgent(agentId: string): void { + this.disabledAgents.delete(agentId); + this.aiSettingsService?.updateAgentSettings(agentId, { enable: true }); + } + + disableAgent(agentId: string): void { + this.disabledAgents.add(agentId); + this.aiSettingsService?.updateAgentSettings(agentId, { enable: false }); + } + + isEnabled(agentId: string): boolean { + return !this.disabledAgents.has(agentId); + } +} diff --git a/packages/ai-core/src/common/agent.ts b/packages/ai-core/src/common/agent.ts new file mode 100644 index 0000000000000..bd37c46f72cd8 --- /dev/null +++ b/packages/ai-core/src/common/agent.ts @@ -0,0 +1,70 @@ +// ***************************************************************************** +// 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 { LanguageModelRequirement } from './language-model'; +import { PromptTemplate } from './prompt-service'; + +export interface AgentSpecificVariables { + name: string; + description: string; + usedInPrompt: boolean; +} + +export const Agent = Symbol('Agent'); +/** + * Agents represent the main functionality of the AI system. They are responsible for processing user input, collecting information from the environment, + * invoking and processing LLM responses, and providing the final response to the user while recording their actions in the AI history. + * + * Agents are meant to cover all use cases, from specialized scenarios to general purpose chat bots. + * + * Agents are encouraged to provide a detailed description of their functionality and their processed inputs. + * They can also declare their used prompt templates, which makes them configurable for the user. + */ +export interface Agent { + /** + * Used to identify an agent, e.g. when it is requesting language models, etc. + * + * @note This parameter might be removed in favor of `name`. Therefore, it is recommended to set `id` to the same value as `name` for now. + */ + readonly id: string; + + /** + * Human-readable name shown to users to identify the agent. Must be unique. + * Use short names without "Agent" or "Chat" (see `tags` for adding further properties). + */ + readonly name: string; + + /** A markdown description of its functionality and its privacy-relevant requirements, including function call handlers that access some data autonomously. */ + readonly description: string; + + /** The list of global variable identifiers this agent needs to clarify its context requirements. See #39. */ + readonly variables: string[]; + + /** The prompt templates introduced and used by this agent. */ + readonly promptTemplates: PromptTemplate[]; + + /** Required language models. This includes the purpose and optional language model selector arguments. See #47. */ + readonly languageModelRequirements: LanguageModelRequirement[]; + + /** A list of tags to filter agents and to display capabilities in the UI */ + readonly tags?: String[]; + + /** The list of local variable identifiers this agent needs to clarify its context requirements. */ + readonly agentSpecificVariables: AgentSpecificVariables[]; + + /** The list of global function identifiers this agent needs to clarify its context requirements. */ + readonly functions: string[]; +} diff --git a/packages/ai-core/src/common/agents-variable-contribution.ts b/packages/ai-core/src/common/agents-variable-contribution.ts new file mode 100644 index 0000000000000..67803cabb9fe0 --- /dev/null +++ b/packages/ai-core/src/common/agents-variable-contribution.ts @@ -0,0 +1,64 @@ +// ***************************************************************************** +// 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 { AIVariable, AIVariableContext, AIVariableContribution, AIVariableResolutionRequest, AIVariableResolver, AIVariableService, ResolvedAIVariable } from './variable-service'; +import { MaybePromise } from '@theia/core'; +import { AgentService } from './agent-service'; + +export const AGENTS_VARIABLE: AIVariable = { + id: 'agents', + name: 'agents', + description: 'Returns the list of agents available in the system' +}; + +export interface ResolvedAgentsVariable extends ResolvedAIVariable { + agents: AgentDescriptor[]; +} + +export interface AgentDescriptor { + id: string; + name: string; + description: string; +} + +@injectable() +export class AgentsVariableContribution implements AIVariableContribution, AIVariableResolver { + + @inject(AgentService) + protected readonly agentService: AgentService; + + registerVariables(service: AIVariableService): void { + service.registerResolver(AGENTS_VARIABLE, this); + } + + canResolve(request: AIVariableResolutionRequest, _context: AIVariableContext): MaybePromise { + if (request.variable.name === AGENTS_VARIABLE.name) { + return 1; + } + return -1; + } + + async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise { + if (request.variable.name === AGENTS_VARIABLE.name) { + const agents = this.agentService.getAgents().map(agent => ({ + id: agent.id, + name: agent.name, + description: agent.description + })); + return { variable: AGENTS_VARIABLE, agents, value: JSON.stringify(agents) }; + } + } +} diff --git a/packages/ai-core/src/common/communication-recording-service.ts b/packages/ai-core/src/common/communication-recording-service.ts new file mode 100644 index 0000000000000..4ae26c47e0e2e --- /dev/null +++ b/packages/ai-core/src/common/communication-recording-service.ts @@ -0,0 +1,47 @@ +// ***************************************************************************** +// 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 { Event } from '@theia/core'; + +export type CommunicationHistory = CommunicationHistoryEntry[]; + +export interface CommunicationHistoryEntry { + agentId: string; + sessionId: string; + timestamp: number; + requestId: string; + request?: string; + response?: string; + responseTime?: number; + messages?: unknown[]; +} + +export type CommunicationRequestEntry = Omit; +export type CommunicationResponseEntry = Omit; + +export const CommunicationRecordingService = Symbol('CommunicationRecordingService'); +export interface CommunicationRecordingService { + recordRequest(requestEntry: CommunicationRequestEntry): void; + readonly onDidRecordRequest: Event; + + recordResponse(responseEntry: CommunicationResponseEntry): void; + readonly onDidRecordResponse: Event; + + getHistory(agentId: string): CommunicationHistory; + + clearHistory(): void; + readonly onStructuralChange: Event; +} diff --git a/packages/ai-core/src/common/index.ts b/packages/ai-core/src/common/index.ts new file mode 100644 index 0000000000000..48070b59007c9 --- /dev/null +++ b/packages/ai-core/src/common/index.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +export * from './agent-service'; +export * from './agent'; +export * from './agents-variable-contribution'; +export * from './communication-recording-service'; +export * from './tool-invocation-registry'; +export * from './language-model-delegate'; +export * from './language-model-util'; +export * from './language-model'; +export * from './prompt-service'; +export * from './prompt-service-util'; +export * from './protocol'; +export * from './today-variable-contribution'; +export * from './tomorrow-variable-contribution'; +export * from './variable-service'; +export * from './settings-service'; diff --git a/packages/ai-core/src/common/language-model-delegate.ts b/packages/ai-core/src/common/language-model-delegate.ts new file mode 100644 index 0000000000000..5edbfe4b18ac7 --- /dev/null +++ b/packages/ai-core/src/common/language-model-delegate.ts @@ -0,0 +1,45 @@ +// ***************************************************************************** +// 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 } from '@theia/core'; +import { LanguageModelMetaData, LanguageModelParsedResponse, LanguageModelRequest, LanguageModelStreamResponsePart, LanguageModelTextResponse } from './language-model'; + +export const LanguageModelDelegateClient = Symbol('LanguageModelDelegateClient'); +export interface LanguageModelDelegateClient { + toolCall(requestId: string, toolId: string, args_string: string): Promise; + send(id: string, token: LanguageModelStreamResponsePart | undefined): void; +} +export const LanguageModelRegistryFrontendDelegate = Symbol('LanguageModelRegistryFrontendDelegate'); +export interface LanguageModelRegistryFrontendDelegate { + getLanguageModelDescriptions(): Promise; +} + +export interface LanguageModelStreamResponseDelegate { + streamId: string; +} +export const isLanguageModelStreamResponseDelegate = (obj: unknown): obj is LanguageModelStreamResponseDelegate => + !!(obj && typeof obj === 'object' && 'streamId' in obj && typeof (obj as { streamId: unknown }).streamId === 'string'); + +export type LanguageModelResponseDelegate = LanguageModelTextResponse | LanguageModelParsedResponse | LanguageModelStreamResponseDelegate; + +export const LanguageModelFrontendDelegate = Symbol('LanguageModelFrontendDelegate'); +export interface LanguageModelFrontendDelegate { + cancel(requestId: string): void; + request(modelId: string, request: LanguageModelRequest, requestId: string, cancellationToken?: CancellationToken): Promise; +} + +export const languageModelRegistryDelegatePath = '/services/languageModelRegistryDelegatePath'; +export const languageModelDelegatePath = '/services/languageModelDelegatePath'; diff --git a/packages/ai-core/src/common/language-model-util.ts b/packages/ai-core/src/common/language-model-util.ts new file mode 100644 index 0000000000000..75546a0e4ac0f --- /dev/null +++ b/packages/ai-core/src/common/language-model-util.ts @@ -0,0 +1,84 @@ +// ***************************************************************************** +// 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 { isLanguageModelParsedResponse, isLanguageModelStreamResponse, isLanguageModelTextResponse, LanguageModelResponse, ToolRequest } from './language-model'; + +/** + * Retrieves the text content from a `LanguageModelResponse` object. + * + * **Important:** For stream responses, the stream can only be consumed once. Calling this function multiple times on the same stream response will return an empty string (`''`) + * on subsequent calls, as the stream will have already been consumed. + * + * @param {LanguageModelResponse} response - The response object, which may contain a text, stream, or parsed response. + * @returns {Promise} - A promise that resolves to the text content of the response. + * @throws {Error} - Throws an error if the response type is not supported or does not contain valid text content. + */ +export const getTextOfResponse = async (response: LanguageModelResponse): Promise => { + if (isLanguageModelTextResponse(response)) { + return response.text; + } else if (isLanguageModelStreamResponse(response)) { + let result = ''; + for await (const chunk of response.stream) { + result += chunk.content ?? ''; + } + return result; + } else if (isLanguageModelParsedResponse(response)) { + return response.content; + } + throw new Error(`Invalid response type ${response}`); +}; + +export const getJsonOfResponse = async (response: LanguageModelResponse): Promise => { + const text = await getTextOfResponse(response); + return getJsonOfText(text); +}; + +export const getJsonOfText = (text: string): unknown => { + if (text.startsWith('```json')) { + const regex = /```json\s*([\s\S]*?)\s*```/g; + let match; + // eslint-disable-next-line no-null/no-null + while ((match = regex.exec(text)) !== null) { + try { + return JSON.parse(match[1]); + } catch (error) { + console.error('Failed to parse JSON:', error); + } + } + } else if (text.startsWith('{') || text.startsWith('[')) { + return JSON.parse(text); + } + throw new Error('Invalid response format'); +}; + +export const toolRequestToPromptText = (toolRequest: ToolRequest): string => { + const parameters = toolRequest.parameters; + let paramsText = ''; + // parameters are supposed to be as a JSON schema. Thus, derive the parameters from its properties definition + if (parameters) { + const properties = parameters.properties; + paramsText = Object.keys(properties) + .map(key => { + const param = properties[key]; + return `${key}: ${param.type}`; + }) + .join(', '); + } + const descriptionText = toolRequest.description + ? `: ${toolRequest.description}` + : ''; + return `You can call function: ${toolRequest.id}(${paramsText})${descriptionText}`; +}; diff --git a/packages/ai-core/src/common/language-model.spec.ts b/packages/ai-core/src/common/language-model.spec.ts new file mode 100644 index 0000000000000..044b839531543 --- /dev/null +++ b/packages/ai-core/src/common/language-model.spec.ts @@ -0,0 +1,86 @@ +// ***************************************************************************** +// 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 { isModelMatching, LanguageModel, LanguageModelSelector } from './language-model'; +import { expect } from 'chai'; + +describe('isModelMatching', () => { + it('returns false with one of two parameter mismatches', () => { + expect( + isModelMatching( + { + name: 'XXX', + family: 'YYY', + }, + { + name: 'gpt-4o', + family: 'YYY', + } + ) + ).eql(false); + }); + it('returns false with two parameter mismatches', () => { + expect( + isModelMatching( + { + name: 'XXX', + family: 'YYY', + }, + { + name: 'gpt-4o', + family: 'ZZZ', + } + ) + ).eql(false); + }); + it('returns true with one parameter match', () => { + expect( + isModelMatching( + { + name: 'gpt-4o', + }, + { + name: 'gpt-4o', + } + ) + ).eql(true); + }); + it('returns true with two parameter matches', () => { + expect( + isModelMatching( + { + name: 'gpt-4o', + family: 'YYY', + }, + { + name: 'gpt-4o', + family: 'YYY', + } + ) + ).eql(true); + }); + it('returns true if there are no parameters in selector', () => { + expect( + isModelMatching( + {}, + { + name: 'gpt-4o', + family: 'YYY', + } + ) + ).eql(true); + }); +}); diff --git a/packages/ai-core/src/common/language-model.ts b/packages/ai-core/src/common/language-model.ts new file mode 100644 index 0000000000000..2945e9ff0d05a --- /dev/null +++ b/packages/ai-core/src/common/language-model.ts @@ -0,0 +1,237 @@ +// ***************************************************************************** +// 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 { ContributionProvider, ILogger, isFunction, isObject, Event, Emitter, CancellationToken } from '@theia/core'; +import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; + +export type MessageActor = 'user' | 'ai' | 'system'; + +export interface LanguageModelRequestMessage { + actor: MessageActor; + type: 'text'; + query: string; +} +export const isLanguageModelRequestMessage = (obj: unknown): obj is LanguageModelRequestMessage => + !!(obj && typeof obj === 'object' && + 'type' in obj && + typeof (obj as { type: unknown }).type === 'string' && + (obj as { type: unknown }).type === 'text' && + 'query' in obj && + typeof (obj as { query: unknown }).query === 'string' + ); +export interface ToolRequest { + id: string; + name: string; + parameters?: { type?: 'object', properties: Record }; + description?: string; + handler: (arg_string: string) => Promise; +} +export interface LanguageModelRequest { + messages: LanguageModelRequestMessage[], + tools?: ToolRequest[]; + response_format?: { type: 'text' } | { type: 'json_object' } | ResponseFormatJsonSchema; + settings?: { [key: string]: unknown }; +} +export interface ResponseFormatJsonSchema { + type: 'json_schema'; + json_schema: { + name: string, + description?: string, + schema?: Record, + strict?: boolean | null + }; +} + +export interface LanguageModelTextResponse { + text: string; +} +export const isLanguageModelTextResponse = (obj: unknown): obj is LanguageModelTextResponse => + !!(obj && typeof obj === 'object' && 'text' in obj && typeof (obj as { text: unknown }).text === 'string'); + +export interface LanguageModelStreamResponsePart { + content?: string | null; + tool_calls?: ToolCall[]; +} + +export interface ToolCall { + id?: string; + function?: { + arguments?: string; + name?: string; + }, + finished?: boolean; + result?: string; +} + +export interface LanguageModelStreamResponse { + stream: AsyncIterable; +} +export const isLanguageModelStreamResponse = (obj: unknown): obj is LanguageModelStreamResponse => + !!(obj && typeof obj === 'object' && 'stream' in obj); + +export interface LanguageModelParsedResponse { + parsed: unknown; + content: string; +} +export const isLanguageModelParsedResponse = (obj: unknown): obj is LanguageModelParsedResponse => + !!(obj && typeof obj === 'object' && 'parsed' in obj && 'content' in obj); + +export type LanguageModelResponse = LanguageModelTextResponse | LanguageModelStreamResponse | LanguageModelParsedResponse; + +/////////////////////////////////////////// +// Language Model Provider +/////////////////////////////////////////// + +export const LanguageModelProvider = Symbol('LanguageModelProvider'); +export type LanguageModelProvider = () => Promise; + +// See also VS Code `ILanguageModelChatMetadata` +export interface LanguageModelMetaData { + readonly id: string; + readonly name?: string; + readonly vendor?: string; + readonly version?: string; + readonly family?: string; + readonly maxInputTokens?: number; + readonly maxOutputTokens?: number; +} + +export namespace LanguageModelMetaData { + export function is(arg: unknown): arg is LanguageModelMetaData { + return isObject(arg) && 'id' in arg; + } +} + +export interface LanguageModel extends LanguageModelMetaData { + request(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise; +} + +export namespace LanguageModel { + export function is(arg: unknown): arg is LanguageModel { + return isObject(arg) && 'id' in arg && isFunction(arg.request); + } +} + +// See also VS Code `ILanguageModelChatSelector` +interface VsCodeLanguageModelSelector { + readonly identifier?: string; + readonly name?: string; + readonly vendor?: string; + readonly version?: string; + readonly family?: string; + readonly tokens?: number; +} + +export interface LanguageModelSelector extends VsCodeLanguageModelSelector { + readonly agent: string; + readonly purpose: string; +} + +export type LanguageModelRequirement = Omit; + +export const LanguageModelRegistry = Symbol('LanguageModelRegistry'); +export interface LanguageModelRegistry { + onChange: Event<{ models: LanguageModel[] }>; + addLanguageModels(models: LanguageModel[]): void; + getLanguageModels(): Promise; + getLanguageModel(id: string): Promise; + removeLanguageModels(id: string[]): void; + selectLanguageModel(request: LanguageModelSelector): Promise; + selectLanguageModels(request: LanguageModelSelector): Promise; +} + +@injectable() +export class DefaultLanguageModelRegistryImpl implements LanguageModelRegistry { + @inject(ILogger) + protected logger: ILogger; + @inject(ContributionProvider) @named(LanguageModelProvider) + protected readonly languageModelContributions: ContributionProvider; + + protected languageModels: LanguageModel[] = []; + + protected markInitialized: () => void; + protected initialized: Promise = new Promise(resolve => { this.markInitialized = resolve; }); + + protected changeEmitter = new Emitter<{ models: LanguageModel[] }>(); + onChange = this.changeEmitter.event; + + @postConstruct() + protected init(): void { + const contributions = this.languageModelContributions.getContributions(); + const promises = contributions.map(provider => provider()); + Promise.allSettled(promises).then(results => { + for (const result of results) { + if (result.status === 'fulfilled') { + this.languageModels.push(...result.value); + } else { + this.logger.error('Failed to add some language models:', result.reason); + } + } + this.markInitialized(); + }); + } + + addLanguageModels(models: LanguageModel[]): void { + models.forEach(model => { + if (this.languageModels.find(lm => lm.id === model.id)) { + console.warn(`Tried to add already existing language model with id ${model.id}. The new model will be ignored.`); + return; + } + this.languageModels.push(model); + this.changeEmitter.fire({ models: this.languageModels }); + }); + } + + async getLanguageModels(): Promise { + await this.initialized; + return this.languageModels; + } + + async getLanguageModel(id: string): Promise { + await this.initialized; + return this.languageModels.find(model => model.id === id); + } + + removeLanguageModels(ids: string[]): void { + ids.forEach(id => { + const index = this.languageModels.findIndex(model => model.id === id); + if (index !== -1) { + this.languageModels.splice(index, 1); + this.changeEmitter.fire({ models: this.languageModels }); + } else { + console.warn(`Language model with id ${id} was requested to be removed, however it does not exist`); + } + }); + } + + async selectLanguageModels(request: LanguageModelSelector): Promise { + await this.initialized; + // TODO check for actor and purpose against settings + return this.languageModels.filter(model => isModelMatching(request, model)); + } + + async selectLanguageModel(request: LanguageModelSelector): Promise { + return (await this.selectLanguageModels(request))[0]; + } +} + +export function isModelMatching(request: LanguageModelSelector, model: LanguageModel): boolean { + return (!request.identifier || model.id === request.identifier) && + (!request.name || model.name === request.name) && + (!request.vendor || model.vendor === request.vendor) && + (!request.version || model.version === request.version) && + (!request.family || model.family === request.family); +} diff --git a/packages/ai-core/src/common/prompt-service-util.ts b/packages/ai-core/src/common/prompt-service-util.ts new file mode 100644 index 0000000000000..0a7cf3e6b34be --- /dev/null +++ b/packages/ai-core/src/common/prompt-service-util.ts @@ -0,0 +1,21 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/** Should match the one from VariableResolverService. The format is `{{variableName:arg}}`. */ +export const PROMPT_VARIABLE_REGEX = /\{\{\s*(.*?)\s*\}\}/g; + +/** Match function/tool references in the prompt. The format is `~{functionId}`. */ +export const PROMPT_FUNCTION_REGEX = /\~\{\s*(.*?)\s*\}/g; diff --git a/packages/ai-core/src/common/prompt-service.spec.ts b/packages/ai-core/src/common/prompt-service.spec.ts new file mode 100644 index 0000000000000..c3cd138d66f66 --- /dev/null +++ b/packages/ai-core/src/common/prompt-service.spec.ts @@ -0,0 +1,98 @@ +// ***************************************************************************** +// 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 'reflect-metadata'; + +import { expect } from 'chai'; +import { Container } from 'inversify'; +import { PromptService, PromptServiceImpl } from './prompt-service'; +import { DefaultAIVariableService, AIVariableService } from './variable-service'; + +describe('PromptService', () => { + let promptService: PromptService; + + beforeEach(() => { + const container = new Container(); + container.bind(PromptService).to(PromptServiceImpl).inSingletonScope(); + + const variableService = new DefaultAIVariableService({ getContributions: () => [] }); + const nameVariable = { id: 'test', name: 'name', description: 'Test name ' }; + variableService.registerResolver(nameVariable, { + canResolve: () => 100, + resolve: async () => ({ variable: nameVariable, value: 'Jane' }) + }); + container.bind(AIVariableService).toConstantValue(variableService); + + promptService = container.get(PromptService); + promptService.storePrompt('1', 'Hello, {{name}}!'); + promptService.storePrompt('2', 'Goodbye, {{name}}!'); + promptService.storePrompt('3', 'Ciao, {{invalid}}!'); + }); + + it('should initialize prompts from PromptCollectionService', () => { + const allPrompts = promptService.getAllPrompts(); + expect(allPrompts['1'].template).to.equal('Hello, {{name}}!'); + expect(allPrompts['2'].template).to.equal('Goodbye, {{name}}!'); + expect(allPrompts['3'].template).to.equal('Ciao, {{invalid}}!'); + }); + + it('should retrieve raw prompt by id', () => { + const rawPrompt = promptService.getRawPrompt('1'); + expect(rawPrompt?.template).to.equal('Hello, {{name}}!'); + }); + + it('should format prompt with provided arguments', async () => { + const formattedPrompt = await promptService.getPrompt('1', { name: 'John' }); + expect(formattedPrompt?.text).to.equal('Hello, John!'); + }); + + it('should store a new prompt', () => { + promptService.storePrompt('3', 'Welcome, {{name}}!'); + const newPrompt = promptService.getRawPrompt('3'); + expect(newPrompt?.template).to.equal('Welcome, {{name}}!'); + }); + + it('should replace placeholders with provided arguments', async () => { + const prompt = await promptService.getPrompt('1', { name: 'John' }); + expect(prompt?.text).to.equal('Hello, John!'); + }); + + it('should use variable service to resolve placeholders if argument value is not provided', async () => { + const prompt = await promptService.getPrompt('1'); + expect(prompt?.text).to.equal('Hello, Jane!'); + }); + + it('should return the prompt even if there are no replacements', async () => { + const prompt = await promptService.getPrompt('3'); + expect(prompt?.text).to.equal('Ciao, {{invalid}}!'); + }); + + it('should return undefined if the prompt id is not found', async () => { + const prompt = await promptService.getPrompt('4'); + expect(prompt).to.be.undefined; + }); + + it('should ignore whitespace in variables', async () => { + promptService.storePrompt('4', 'Hello, {{name }}!'); + promptService.storePrompt('5', 'Hello, {{ name}}!'); + promptService.storePrompt('6', 'Hello, {{ name }}!'); + promptService.storePrompt('7', 'Hello, {{ name }}!'); + for (let i = 4; i <= 7; i++) { + const prompt = await promptService.getPrompt(`${i}`, { name: 'John' }); + expect(prompt?.text).to.equal('Hello, John!'); + } + }); +}); diff --git a/packages/ai-core/src/common/prompt-service.ts b/packages/ai-core/src/common/prompt-service.ts new file mode 100644 index 0000000000000..44d8fb0622d66 --- /dev/null +++ b/packages/ai-core/src/common/prompt-service.ts @@ -0,0 +1,253 @@ +// ***************************************************************************** +// 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 { URI, Event } from '@theia/core'; +import { inject, injectable, optional } from '@theia/core/shared/inversify'; +import { AIVariableService } from './variable-service'; +import { ToolInvocationRegistry } from './tool-invocation-registry'; +import { toolRequestToPromptText } from './language-model-util'; +import { ToolRequest } from './language-model'; +import { PROMPT_VARIABLE_REGEX, PROMPT_FUNCTION_REGEX } from './prompt-service-util'; + +export interface PromptTemplate { + id: string; + template: string; +} + +export interface PromptMap { [id: string]: PromptTemplate } + +export interface ResolvedPromptTemplate { + id: string; + /** The resolved prompt text with variables and function requests being replaced. */ + text: string; + /** All functions referenced in the prompt template. */ + functionDescriptions?: Map; +} + +export const PromptService = Symbol('PromptService'); +export interface PromptService { + /** + * Retrieve the raw {@link PromptTemplate} object. + * @param id the id of the {@link PromptTemplate} + */ + getRawPrompt(id: string): PromptTemplate | undefined; + /** + * Retrieve the default raw {@link PromptTemplate} object. + * @param id the id of the {@link PromptTemplate} + */ + getDefaultRawPrompt(id: string): PromptTemplate | undefined; + /** + * Allows to directly replace placeholders in the prompt. The supported format is 'Hi {{name}}!'. + * The placeholder is then searched inside the args object and replaced. + * Function references are also supported via format '~{functionId}'. + * @param id the id of the prompt + * @param args the object with placeholders, mapping the placeholder key to the value + */ + getPrompt(id: string, args?: { [key: string]: unknown }): Promise; + /** + * Manually add a prompt to the list of prompts. + * @param id the id of the prompt + * @param prompt the prompt template to store + */ + storePrompt(id: string, prompt: string): void; + /** + * Return all known prompts as a {@link PromptMap map}. + */ + getAllPrompts(): PromptMap; +} + +export interface CustomAgentDescription { + id: string; + name: string; + description: string; + prompt: string; + defaultLLM: string; +} +export namespace CustomAgentDescription { + export function is(entry: unknown): entry is CustomAgentDescription { + // eslint-disable-next-line no-null/no-null + return typeof entry === 'object' && entry !== null + && 'id' in entry && typeof entry.id === 'string' + && 'name' in entry && typeof entry.name === 'string' + && 'description' in entry && typeof entry.description === 'string' + && 'prompt' in entry + && typeof entry.prompt === 'string' + && 'defaultLLM' in entry + && typeof entry.defaultLLM === 'string'; + } + export function equals(a: CustomAgentDescription, b: CustomAgentDescription): boolean { + return a.id === b.id && a.name === b.name && a.description === b.description && a.prompt === b.prompt && a.defaultLLM === b.defaultLLM; + } +} + +export const PromptCustomizationService = Symbol('PromptCustomizationService'); +export interface PromptCustomizationService { + /** + * Whether there is a customization for a {@link PromptTemplate} object + * @param id the id of the {@link PromptTemplate} to check + */ + isPromptTemplateCustomized(id: string): boolean; + + /** + * Returns the customization of {@link PromptTemplate} object or undefined if there is none + * @param id the id of the {@link PromptTemplate} to check + */ + getCustomizedPromptTemplate(id: string): string | undefined + + /** + * Edit the template. If the content is specified, is will be + * used to customize the template. Otherwise, the behavior depends + * on the implementation. Implementation may for example decide to + * open an editor, or request more information from the user, ... + * @param id the template id. + * @param content optional content to customize the template. + */ + editTemplate(id: string, content?: string): void; + + /** + * Reset the template to its default value. + * @param id the template id. + */ + resetTemplate(id: string): void; + + /** + * Return the template id for a given template file. + * @param uri the uri of the template file + */ + getTemplateIDFromURI(uri: URI): string | undefined; + + /** + * Event which is fired when the prompt template is changed. + */ + readonly onDidChangePrompt: Event; + + /** + * Return all custom agents. + * @returns all custom agents + */ + getCustomAgents(): Promise; + + /** + * Event which is fired when custom agents are modified. + */ + readonly onDidChangeCustomAgents: Event; + + /** + * Open the custom agent yaml file. + */ + openCustomAgentYaml(): void; +} + +@injectable() +export class PromptServiceImpl implements PromptService { + @inject(PromptCustomizationService) @optional() + protected readonly customizationService: PromptCustomizationService | undefined; + + @inject(AIVariableService) @optional() + protected readonly variableService: AIVariableService | undefined; + + @inject(ToolInvocationRegistry) @optional() + protected readonly toolInvocationRegistry: ToolInvocationRegistry | undefined; + + protected _prompts: PromptMap = {}; + + getRawPrompt(id: string): PromptTemplate | undefined { + if (this.customizationService !== undefined && this.customizationService.isPromptTemplateCustomized(id)) { + const template = this.customizationService.getCustomizedPromptTemplate(id); + if (template !== undefined) { + return { id, template }; + } + } + return this.getDefaultRawPrompt(id); + } + getDefaultRawPrompt(id: string): PromptTemplate | undefined { + return this._prompts[id]; + } + async getPrompt(id: string, args?: { [key: string]: unknown }): Promise { + const prompt = this.getRawPrompt(id); + if (prompt === undefined) { + return undefined; + } + + const matches = [...prompt.template.matchAll(PROMPT_VARIABLE_REGEX)]; + const variableAndArgReplacements = await Promise.all(matches.map(async match => { + const completeText = match[0]; + const variableAndArg = match[1]; + let variableName = variableAndArg; + let argument: string | undefined; + const parts = variableAndArg.split(':', 2); + if (parts.length > 1) { + variableName = parts[0]; + argument = parts[1]; + } + return { + placeholder: completeText, + value: String(args?.[variableAndArg] ?? (await this.variableService?.resolveVariable({ + variable: variableName, + arg: argument + }, {}))?.value ?? completeText) + }; + })); + + const functionMatches = [...prompt.template.matchAll(PROMPT_FUNCTION_REGEX)]; + const functions = new Map(); + const functionReplacements = functionMatches.map(match => { + const completeText = match[0]; + const functionId = match[1]; + const toolRequest = this.toolInvocationRegistry?.getFunction(functionId); + if (toolRequest) { + functions.set(toolRequest.id, toolRequest); + } + return { + placeholder: completeText, + value: toolRequest ? toolRequestToPromptText(toolRequest) : completeText + }; + }); + + let resolvedTemplate = prompt.template; + const replacements = [...variableAndArgReplacements, ...functionReplacements]; + replacements.forEach(replacement => resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, replacement.value)); + return { + id, + text: resolvedTemplate, + functionDescriptions: functions.size > 0 ? functions : undefined + }; + } + getAllPrompts(): PromptMap { + if (this.customizationService !== undefined) { + const myCustomization = this.customizationService; + const result: PromptMap = {}; + Object.keys(this._prompts).forEach(id => { + if (myCustomization.isPromptTemplateCustomized(id)) { + const template = myCustomization.getCustomizedPromptTemplate(id); + if (template !== undefined) { + result[id] = { id, template }; + } else { + result[id] = { ...this._prompts[id] }; + } + } else { + result[id] = { ...this._prompts[id] }; + } + }); + return result; + } else { + return { ...this._prompts }; + } + } + storePrompt(id: string, prompt: string): void { + this._prompts[id] = { id, template: prompt }; + } +} diff --git a/packages/ai-core/src/common/protocol.ts b/packages/ai-core/src/common/protocol.ts new file mode 100644 index 0000000000000..ec1c3dbfde4b6 --- /dev/null +++ b/packages/ai-core/src/common/protocol.ts @@ -0,0 +1,23 @@ +// ***************************************************************************** +// 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 { LanguageModelMetaData } from './language-model'; + +export const LanguageModelRegistryClient = Symbol('LanguageModelRegistryClient'); +export interface LanguageModelRegistryClient { + languageModelAdded(metadata: LanguageModelMetaData): void; + languageModelRemoved(id: string): void; +} diff --git a/packages/ai-core/src/common/settings-service.ts b/packages/ai-core/src/common/settings-service.ts new file mode 100644 index 0000000000000..007daec366250 --- /dev/null +++ b/packages/ai-core/src/common/settings-service.ts @@ -0,0 +1,33 @@ +// ***************************************************************************** +// 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 { Event } from '@theia/core'; +import { LanguageModelRequirement } from './language-model'; + +export const AISettingsService = Symbol('AISettingsService'); +/** + * Service to store and retrieve settings on a per-agent basis. + */ +export interface AISettingsService { + updateAgentSettings(agent: string, agentSettings: Partial): Promise; + getAgentSettings(agent: string): Promise; + getSettings(): Promise; + onDidChange: Event; +} +export type AISettings = Record; +export interface AgentSettings { + languageModelRequirements?: LanguageModelRequirement[]; + enable?: boolean; +} diff --git a/packages/ai-core/src/common/today-variable-contribution.ts b/packages/ai-core/src/common/today-variable-contribution.ts new file mode 100644 index 0000000000000..a155618ffe85c --- /dev/null +++ b/packages/ai-core/src/common/today-variable-contribution.ts @@ -0,0 +1,67 @@ +// ***************************************************************************** +// 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 { MaybePromise } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { AIVariable, ResolvedAIVariable, AIVariableContribution, AIVariableResolver, AIVariableService, AIVariableResolutionRequest, AIVariableContext } from './variable-service'; + +export namespace TodayVariableArgs { + export const IN_UNIX_SECONDS = 'inUnixSeconds'; + export const IN_ISO_8601 = 'inIso8601'; +} + +export const TODAY_VARIABLE: AIVariable = { + id: 'today-provider', + description: 'Does something for today', + name: 'today', + args: [ + { name: TodayVariableArgs.IN_ISO_8601, description: 'Returns the current date in ISO 8601 format' }, + { name: TodayVariableArgs.IN_UNIX_SECONDS, description: 'Returns the current date in unix seconds format' } + ] +}; + +export interface ResolvedTodayVariable extends ResolvedAIVariable { + date: Date; +} + +@injectable() +export class TodayVariableContribution implements AIVariableContribution, AIVariableResolver { + registerVariables(service: AIVariableService): void { + service.registerResolver(TODAY_VARIABLE, this); + } + + canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise { + return 1; + } + + async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise { + if (request.variable.name === TODAY_VARIABLE.name) { + return this.resolveTodayVariable(request); + } + return undefined; + } + + private resolveTodayVariable(request: AIVariableResolutionRequest): ResolvedTodayVariable { + const date = new Date(); + if (request.arg === TodayVariableArgs.IN_ISO_8601) { + return { variable: request.variable, value: date.toISOString(), date }; + } + if (request.arg === TodayVariableArgs.IN_UNIX_SECONDS) { + return { variable: request.variable, value: Math.round(date.getTime() / 1000).toString(), date }; + } + return { variable: request.variable, value: date.toDateString(), date }; + } +} + diff --git a/packages/ai-core/src/common/tomorrow-variable-contribution.ts b/packages/ai-core/src/common/tomorrow-variable-contribution.ts new file mode 100644 index 0000000000000..8575505cfef82 --- /dev/null +++ b/packages/ai-core/src/common/tomorrow-variable-contribution.ts @@ -0,0 +1,66 @@ +// ***************************************************************************** +// 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 { MaybePromise } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { AIVariable, AIVariableContext, AIVariableContribution, AIVariableResolutionRequest, AIVariableResolver, AIVariableService, ResolvedAIVariable } from './variable-service'; + +export namespace TomorrowVariableArgs { + export const IN_UNIX_SECONDS = 'inUnixSeconds'; + export const IN_ISO_8601 = 'inIso8601'; +} + +export const TOMORROW_VARIABLE: AIVariable = { + id: 'tomorrow-provider', + description: 'Does something for tomorrow', + name: 'tomorrow', + args: [ + { name: TomorrowVariableArgs.IN_ISO_8601, description: 'Returns the current date in ISO 8601 format' }, + { name: TomorrowVariableArgs.IN_UNIX_SECONDS, description: 'Returns the current date in unix seconds format' } + ] +}; + +export interface ResolvedTomorrowVariable extends ResolvedAIVariable { + date: Date; +} + +@injectable() +export class TomorrowVariableContribution implements AIVariableContribution, AIVariableResolver { + registerVariables(service: AIVariableService): void { + service.registerResolver(TOMORROW_VARIABLE, this); + } + + canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise { + return 1; + } + + async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise { + if (request.variable.name === TOMORROW_VARIABLE.name) { + return this.resolveTomorrowVariable(request); + } + return undefined; + } + + private resolveTomorrowVariable(request: AIVariableResolutionRequest): ResolvedTomorrowVariable { + const date = new Date(+new Date() + 86400000); + if (request.arg === TomorrowVariableArgs.IN_ISO_8601) { + return { variable: request.variable, value: date.toISOString(), date }; + } + if (request.arg === TomorrowVariableArgs.IN_UNIX_SECONDS) { + return { variable: request.variable, value: Math.round(date.getTime() / 1000).toString(), date }; + } + return { variable: request.variable, value: date.toDateString(), date }; + } +} diff --git a/packages/ai-core/src/common/tool-invocation-registry.ts b/packages/ai-core/src/common/tool-invocation-registry.ts new file mode 100644 index 0000000000000..2ebde1921103a --- /dev/null +++ b/packages/ai-core/src/common/tool-invocation-registry.ts @@ -0,0 +1,79 @@ +// ***************************************************************************** +// 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, named, postConstruct } from '@theia/core/shared/inversify'; +import { ToolRequest } from './language-model'; +import { ContributionProvider } from '@theia/core'; + +export const ToolInvocationRegistry = Symbol('ToolInvocationRegistry'); + +/** + * Registry for all the function calls available to Agents. + */ +export interface ToolInvocationRegistry { + registerTool(tool: ToolRequest): void; + + getFunction(toolId: string): ToolRequest | undefined; + + getFunctions(...toolIds: string[]): ToolRequest[]; +} + +export const ToolProvider = Symbol('ToolProvider'); +export interface ToolProvider { + getTool(): ToolRequest; +} + +@injectable() +export class ToolInvocationRegistryImpl implements ToolInvocationRegistry { + + private tools: Map = new Map(); + + @inject(ContributionProvider) + @named(ToolProvider) + private providers: ContributionProvider; + + @postConstruct() + init(): void { + this.providers.getContributions().forEach(provider => { + this.registerTool(provider.getTool()); + }); + } + + registerTool(tool: ToolRequest): void { + if (this.tools.has(tool.id)) { + console.warn(`Function with id ${tool.id} is already registered.`); + } else { + this.tools.set(tool.id, tool); + } + } + + getFunction(toolId: string): ToolRequest | undefined { + return this.tools.get(toolId); + } + + getFunctions(...toolIds: string[]): ToolRequest[] { + const tools: ToolRequest[] = toolIds.map(toolId => { + const tool = this.tools.get(toolId); + if (tool) { + return tool; + } else { + throw new Error(`Function with id ${toolId} does not exist.`); + } + }); + return tools; + } +} + diff --git a/packages/ai-core/src/common/variable-service.ts b/packages/ai-core/src/common/variable-service.ts new file mode 100644 index 0000000000000..833d322eed48f --- /dev/null +++ b/packages/ai-core/src/common/variable-service.ts @@ -0,0 +1,177 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatVariables.ts + +import { ContributionProvider, Disposable, Emitter, ILogger, MaybePromise, Prioritizeable, Event } from '@theia/core'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; + +export interface AIVariable { + /** provider id */ + id: string; + /** variable name */ + name: string; + /** variable description */ + description: string; + args?: AIVariableDescription[]; +} + +export interface AIVariableDescription { + name: string; + description: string; +} + +export interface ResolvedAIVariable { + variable: AIVariable; + value: string; +} + +export interface AIVariableResolutionRequest { + variable: AIVariable; + arg?: string; +} + +export interface AIVariableContext { +} + +export type AIVariableArg = string | { variable: string, arg?: string } | AIVariableResolutionRequest; + +export interface AIVariableResolver { + canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise, + resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise; +} + +export const AIVariableService = Symbol('AIVariableService'); +export interface AIVariableService { + hasVariable(name: string): boolean; + getVariable(name: string): Readonly | undefined; + getVariables(): Readonly[]; + unregisterVariable(name: string): void; + readonly onDidChangeVariables: Event; + + registerResolver(variable: AIVariable, resolver: AIVariableResolver): Disposable; + unregisterResolver(variable: AIVariable, resolver: AIVariableResolver): void; + getResolver(name: string, arg: string | undefined, context: AIVariableContext): Promise; + + resolveVariable(variable: AIVariableArg, context: AIVariableContext): Promise; +} + +export const AIVariableContribution = Symbol('AIVariableContribution'); +export interface AIVariableContribution { + registerVariables(service: AIVariableService): void; +} + +@injectable() +export class DefaultAIVariableService implements AIVariableService { + protected variables = new Map(); + protected resolvers = new Map(); + + protected readonly onDidChangeVariablesEmitter = new Emitter(); + readonly onDidChangeVariables: Event = this.onDidChangeVariablesEmitter.event; + + @inject(ILogger) protected logger: ILogger; + + constructor( + @inject(ContributionProvider) @named(AIVariableContribution) + protected readonly contributionProvider: ContributionProvider + ) { + } + + protected initContributions(): void { + this.contributionProvider.getContributions().forEach(contribution => contribution.registerVariables(this)); + } + + protected getKey(name: string): string { + return `${name.toLowerCase()}`; + } + + async getResolver(name: string, arg: string | undefined, context: AIVariableContext): Promise { + const resolvers = await this.prioritize(name, arg, context); + return resolvers[0]; + } + + protected getResolvers(name: string): AIVariableResolver[] { + return this.resolvers.get(this.getKey(name)) ?? []; + } + + protected async prioritize(name: string, arg: string | undefined, context: AIVariableContext): Promise { + const variable = this.getVariable(name); + if (!variable) { + return []; + } + const prioritized = await Prioritizeable.prioritizeAll(this.getResolvers(name), async resolver => { + try { + return await resolver.canResolve({ variable, arg }, context); + } catch { + return 0; + } + }); + return prioritized.map(p => p.value); + } + + hasVariable(name: string): boolean { + return !!this.getVariable(name); + } + + getVariable(name: string): Readonly | undefined { + return this.variables.get(this.getKey(name)); + } + + getVariables(): Readonly[] { + return [...this.variables.values()]; + } + + registerResolver(variable: AIVariable, resolver: AIVariableResolver): Disposable { + const key = this.getKey(variable.name); + if (!this.variables.get(key)) { + this.variables.set(key, variable); + this.onDidChangeVariablesEmitter.fire(); + } + const resolvers = this.resolvers.get(key) ?? []; + resolvers.push(resolver); + this.resolvers.set(key, resolvers); + return Disposable.create(() => this.unregisterResolver(variable, resolver)); + } + + unregisterResolver(variable: AIVariable, resolver: AIVariableResolver): void { + const key = this.getKey(variable.name); + const registeredResolvers = this.resolvers.get(key); + registeredResolvers?.splice(registeredResolvers.indexOf(resolver), 1); + if (registeredResolvers?.length === 0) { + this.unregisterVariable(variable.name); + } + } + + unregisterVariable(name: string): void { + this.variables.delete(this.getKey(name)); + this.resolvers.delete(this.getKey(name)); + this.onDidChangeVariablesEmitter.fire(); + } + + async resolveVariable(request: AIVariableArg, context: AIVariableContext): Promise { + const variableName = typeof request === 'string' ? request : typeof request.variable === 'string' ? request.variable : request.variable.name; + const variable = this.getVariable(variableName); + if (!variable) { + return undefined; + } + const arg = typeof request === 'string' ? undefined : request.arg; + const resolver = await this.getResolver(variableName, arg, context); + return resolver?.resolve({ variable, arg }, context); + } +} diff --git a/packages/ai-core/src/node/ai-core-backend-module.ts b/packages/ai-core/src/node/ai-core-backend-module.ts new file mode 100644 index 0000000000000..5c23c7d37f1ac --- /dev/null +++ b/packages/ai-core/src/node/ai-core-backend-module.ts @@ -0,0 +1,83 @@ +// ***************************************************************************** +// 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { + LanguageModelFrontendDelegateImpl, + LanguageModelRegistryFrontendDelegateImpl, +} from './language-model-frontend-delegate'; +import { + ConnectionHandler, + RpcConnectionHandler, + bindContributionProvider, +} from '@theia/core'; +import { + LanguageModelRegistry, + LanguageModelProvider, + PromptService, + PromptServiceImpl, + LanguageModelDelegateClient, + LanguageModelFrontendDelegate, + LanguageModelRegistryFrontendDelegate, + languageModelDelegatePath, + languageModelRegistryDelegatePath, + LanguageModelRegistryClient +} from '../common'; +import { BackendLanguageModelRegistry } from './backend-language-model-registry'; + +export default new ContainerModule(bind => { + bindContributionProvider(bind, LanguageModelProvider); + bind(BackendLanguageModelRegistry).toSelf().inSingletonScope(); + bind(LanguageModelRegistry).toService(BackendLanguageModelRegistry); + + bind(LanguageModelRegistryFrontendDelegate).to(LanguageModelRegistryFrontendDelegateImpl).inSingletonScope(); + bind(ConnectionHandler) + .toDynamicValue( + ctx => + new RpcConnectionHandler( + languageModelRegistryDelegatePath, + client => { + const registryDelegate = ctx.container.get( + LanguageModelRegistryFrontendDelegate + ); + registryDelegate.setClient(client); + return registryDelegate; + } + ) + ) + .inSingletonScope(); + + bind(LanguageModelFrontendDelegateImpl).toSelf().inSingletonScope(); + bind(LanguageModelFrontendDelegate).toService(LanguageModelFrontendDelegateImpl); + bind(ConnectionHandler) + .toDynamicValue( + ({ container }) => + new RpcConnectionHandler( + languageModelDelegatePath, + client => { + const service = + container.get( + LanguageModelFrontendDelegateImpl + ); + service.setClient(client); + return service; + } + ) + ) + .inSingletonScope(); + + bind(PromptServiceImpl).toSelf().inSingletonScope(); + bind(PromptService).toService(PromptServiceImpl); +}); diff --git a/packages/ai-core/src/node/backend-language-model-registry.ts b/packages/ai-core/src/node/backend-language-model-registry.ts new file mode 100644 index 0000000000000..1c953988630b1 --- /dev/null +++ b/packages/ai-core/src/node/backend-language-model-registry.ts @@ -0,0 +1,59 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; +import { DefaultLanguageModelRegistryImpl, LanguageModel, LanguageModelMetaData, LanguageModelRegistryClient } from '../common'; + +/** + * Notifies a client whenever a model is added or removed + */ +@injectable() +export class BackendLanguageModelRegistry extends DefaultLanguageModelRegistryImpl { + + private client: LanguageModelRegistryClient | undefined; + + setClient(client: LanguageModelRegistryClient): void { + this.client = client; + } + + override addLanguageModels(models: LanguageModel[]): void { + const modelsLength = this.languageModels.length; + super.addLanguageModels(models); + // only notify for models which were really added + for (let i = modelsLength; i < this.languageModels.length; i++) { + this.client?.languageModelAdded(this.mapToMetaData(this.languageModels[i])); + } + } + + override removeLanguageModels(ids: string[]): void { + super.removeLanguageModels(ids); + for (const id of ids) { + this.client?.languageModelRemoved(id); + } + } + + mapToMetaData(model: LanguageModel): LanguageModelMetaData { + return { + id: model.id, + name: model.name, + vendor: model.vendor, + version: model.version, + family: model.family, + maxInputTokens: model.maxInputTokens, + maxOutputTokens: model.maxOutputTokens, + }; + } +} diff --git a/packages/ai-core/src/node/language-model-frontend-delegate.ts b/packages/ai-core/src/node/language-model-frontend-delegate.ts new file mode 100644 index 0000000000000..0255c0111aa6a --- /dev/null +++ b/packages/ai-core/src/node/language-model-frontend-delegate.ts @@ -0,0 +1,116 @@ +// ***************************************************************************** +// 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 { CancellationToken, CancellationTokenSource, ILogger, generateUuid } from '@theia/core'; +import { + LanguageModelMetaData, + LanguageModelRegistry, + LanguageModelRequest, + isLanguageModelStreamResponse, + isLanguageModelTextResponse, + LanguageModelStreamResponsePart, + LanguageModelDelegateClient, + LanguageModelFrontendDelegate, + LanguageModelRegistryFrontendDelegate, + LanguageModelResponseDelegate, + LanguageModelRegistryClient, + isLanguageModelParsedResponse, +} from '../common'; +import { BackendLanguageModelRegistry } from './backend-language-model-registry'; + +@injectable() +export class LanguageModelRegistryFrontendDelegateImpl implements LanguageModelRegistryFrontendDelegate { + + @inject(LanguageModelRegistry) + private registry: BackendLanguageModelRegistry; + + setClient(client: LanguageModelRegistryClient): void { + this.registry.setClient(client); + } + + async getLanguageModelDescriptions(): Promise { + return (await this.registry.getLanguageModels()).map(model => this.registry.mapToMetaData(model)); + } +} + +@injectable() +export class LanguageModelFrontendDelegateImpl implements LanguageModelFrontendDelegate { + + @inject(LanguageModelRegistry) + private registry: LanguageModelRegistry; + + @inject(ILogger) + private logger: ILogger; + + private frontendDelegateClient: LanguageModelDelegateClient; + private requestCancellationTokenMap: Map = new Map(); + + setClient(client: LanguageModelDelegateClient): void { + this.frontendDelegateClient = client; + } + + cancel(requestId: string): void { + this.requestCancellationTokenMap.get(requestId)?.cancel(); + } + + async request( + modelId: string, + request: LanguageModelRequest, + requestId: string, + cancellationToken?: CancellationToken + ): Promise { + const model = await this.registry.getLanguageModel(modelId); + if (!model) { + throw new Error( + `Request was sent to non-existent language model ${modelId}` + ); + } + request.tools?.forEach(tool => { + tool.handler = async args_string => this.frontendDelegateClient.toolCall(requestId, tool.id, args_string); + }); + if (cancellationToken) { + const tokenSource = new CancellationTokenSource(); + cancellationToken = tokenSource.token; + this.requestCancellationTokenMap.set(requestId, tokenSource); + } + const response = await model.request(request, cancellationToken); + if (isLanguageModelTextResponse(response) || isLanguageModelParsedResponse(response)) { + return response; + } + if (isLanguageModelStreamResponse(response)) { + const delegate = { + streamId: generateUuid(), + }; + this.sendTokens(delegate.streamId, response.stream); + return delegate; + } + this.logger.error( + `Received unexpected response from language model ${modelId}. Trying to continue without touching the response.`, + response + ); + return response; + } + + protected sendTokens(id: string, stream: AsyncIterable): void { + (async () => { + for await (const token of stream) { + this.frontendDelegateClient.send(id, token); + } + this.frontendDelegateClient.send(id, undefined); + })(); + } +} diff --git a/packages/ai-core/tsconfig.json b/packages/ai-core/tsconfig.json new file mode 100644 index 0000000000000..4ee37e165b355 --- /dev/null +++ b/packages/ai-core/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + }, + { + "path": "../editor" + }, + { + "path": "../filesystem" + }, + { + "path": "../monaco" + }, + { + "path": "../output" + }, + { + "path": "../variable-resolver" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/ai-history/.eslintrc.js b/packages/ai-history/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-history/.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-history/README.md b/packages/ai-history/README.md new file mode 100644 index 0000000000000..6992a4ae49041 --- /dev/null +++ b/packages/ai-history/README.md @@ -0,0 +1,31 @@ +
    + +
    + +theia-ext-logo + +

    ECLIPSE THEIA - AI History EXTENSION

    + +
    + +
    + +## Description + +The `@theia/ai-history` extension offers a framework for agents to record their requests and responses. +It also offers a view to inspect the history. + + +## 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-history/package.json b/packages/ai-history/package.json new file mode 100644 index 0000000000000..f4c4cd224670d --- /dev/null +++ b/packages/ai-history/package.json @@ -0,0 +1,53 @@ +{ + "name": "@theia/ai-history", + "version": "1.54.0", + "description": "Theia - AI communication history", + "dependencies": { + "@theia/ai-core": "1.54.0", + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/output": "1.54.0", + "@theia/workspace": "1.54.0", + "minimatch": "^5.1.0", + "tslib": "^2.6.2" + }, + "main": "lib/common", + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/ai-history-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-history/src/browser/ai-history-communication-card.tsx b/packages/ai-history/src/browser/ai-history-communication-card.tsx new file mode 100644 index 0000000000000..3320d13692729 --- /dev/null +++ b/packages/ai-history/src/browser/ai-history-communication-card.tsx @@ -0,0 +1,48 @@ +// ***************************************************************************** +// 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 { CommunicationHistoryEntry } from '@theia/ai-core'; +import * as React from '@theia/core/shared/react'; + +export interface CommunicationCardProps { + entry: CommunicationHistoryEntry; +} + +export const CommunicationCard: React.FC = ({ entry }) => ( +
    +
    + Request ID: {entry.requestId} + Session ID: {entry.sessionId} +
    +
    + {entry.request && ( +
    +

    Request

    +
    {entry.request}
    +
    + )} + {entry.response && ( +
    +

    Response

    +
    {entry.response}
    +
    + )} +
    +
    + Timestamp: {new Date(entry.timestamp).toLocaleString()} + {entry.responseTime && Response Time: {entry.responseTime}ms} +
    +
    +); diff --git a/packages/ai-history/src/browser/ai-history-contribution.ts b/packages/ai-history/src/browser/ai-history-contribution.ts new file mode 100644 index 0000000000000..38d87ce4aa425 --- /dev/null +++ b/packages/ai-history/src/browser/ai-history-contribution.ts @@ -0,0 +1,142 @@ +// ***************************************************************************** +// 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 { FrontendApplication, codicon } from '@theia/core/lib/browser'; +import { AIViewContribution } from '@theia/ai-core/lib/browser'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { AIHistoryView } from './ai-history-widget'; +import { Command, CommandRegistry, Emitter } from '@theia/core'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { CommunicationRecordingService } from '@theia/ai-core'; + +export const AI_HISTORY_TOGGLE_COMMAND_ID = 'aiHistory:toggle'; +export const OPEN_AI_HISTORY_VIEW = Command.toLocalizedCommand({ + id: 'aiHistory:open', + label: 'Open AI History view', +}); + +export const AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY = Command.toLocalizedCommand({ + id: 'aiHistory:sortChronologically', + label: 'AI History: Sort chronologically', + iconClass: codicon('arrow-down') +}); + +export const AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY = Command.toLocalizedCommand({ + id: 'aiHistory:sortReverseChronologically', + label: 'AI History: Sort reverse chronologically', + iconClass: codicon('arrow-up') +}); + +export const AI_HISTORY_VIEW_CLEAR = Command.toLocalizedCommand({ + id: 'aiHistory:clear', + label: 'AI History: Clear History', + iconClass: codicon('clear-all') +}); + +@injectable() +export class AIHistoryViewContribution extends AIViewContribution implements TabBarToolbarContribution { + @inject(CommunicationRecordingService) private recordingService: CommunicationRecordingService; + + constructor() { + super({ + widgetId: AIHistoryView.ID, + widgetName: AIHistoryView.LABEL, + defaultWidgetOptions: { + area: 'bottom', + rank: 100 + }, + toggleCommandId: AI_HISTORY_TOGGLE_COMMAND_ID, + }); + } + + async initializeLayout(_app: FrontendApplication): Promise { + await this.openView(); + } + + override registerCommands(registry: CommandRegistry): void { + super.registerCommands(registry); + registry.registerCommand(OPEN_AI_HISTORY_VIEW, { + execute: () => this.openView({ activate: true }), + }); + registry.registerCommand(AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY, { + isEnabled: widget => this.withHistoryWidget(widget, historyView => !historyView.isChronological), + isVisible: widget => this.withHistoryWidget(widget, historyView => !historyView.isChronological), + execute: widget => this.withHistoryWidget(widget, historyView => { + historyView.sortHistory(true); + return true; + }) + }); + registry.registerCommand(AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY, { + isEnabled: widget => this.withHistoryWidget(widget, historyView => historyView.isChronological), + isVisible: widget => this.withHistoryWidget(widget, historyView => historyView.isChronological), + execute: widget => this.withHistoryWidget(widget, historyView => { + historyView.sortHistory(false); + return true; + }) + }); + registry.registerCommand(AI_HISTORY_VIEW_CLEAR, { + isEnabled: widget => this.withHistoryWidget(widget), + isVisible: widget => this.withHistoryWidget(widget), + execute: widget => this.withHistoryWidget(widget, () => { + this.clearHistory(); + return true; + }) + }); + } + public clearHistory(): void { + this.recordingService.clearHistory(); + } + + protected withHistoryWidget( + widget: unknown = this.tryGetWidget(), + predicate: (output: AIHistoryView) => boolean = () => true + ): boolean | false { + return widget instanceof AIHistoryView ? predicate(widget) : false; + } + + protected readonly onAIHistoryWidgetStateChangedEmitter = new Emitter(); + protected readonly onAIHistoryWidgettStateChanged = this.onAIHistoryWidgetStateChangedEmitter.event; + + @postConstruct() + protected override init(): void { + super.init(); + this.widget.then(widget => { + widget.onStateChanged(() => this.onAIHistoryWidgetStateChangedEmitter.fire()); + }); + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id, + command: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id, + tooltip: 'Sort chronologically', + isVisible: widget => this.withHistoryWidget(widget), + onDidChange: this.onAIHistoryWidgettStateChanged + }); + registry.registerItem({ + id: AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY.id, + command: AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY.id, + tooltip: 'Sort reverse chronologically', + isVisible: widget => this.withHistoryWidget(widget), + onDidChange: this.onAIHistoryWidgettStateChanged + }); + registry.registerItem({ + id: AI_HISTORY_VIEW_CLEAR.id, + command: AI_HISTORY_VIEW_CLEAR.id, + tooltip: 'Clear History of all agents', + isVisible: widget => this.withHistoryWidget(widget) + }); + } +} diff --git a/packages/ai-history/src/browser/ai-history-frontend-module.ts b/packages/ai-history/src/browser/ai-history-frontend-module.ts new file mode 100644 index 0000000000000..460dc9a6a06a6 --- /dev/null +++ b/packages/ai-history/src/browser/ai-history-frontend-module.ts @@ -0,0 +1,44 @@ +// ***************************************************************************** +// 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 { CommunicationRecordingService } from '@theia/ai-core'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { DefaultCommunicationRecordingService } from '../common/communication-recording-service'; +import { bindViewContribution, WidgetFactory } from '@theia/core/lib/browser'; +import { ILogger } from '@theia/core'; +import { AIHistoryViewContribution } from './ai-history-contribution'; +import { AIHistoryView } from './ai-history-widget'; +import '../../src/browser/style/ai-history.css'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; + +export default new ContainerModule(bind => { + bind(DefaultCommunicationRecordingService).toSelf().inSingletonScope(); + bind(CommunicationRecordingService).toService(DefaultCommunicationRecordingService); + + bind(ILogger).toDynamicValue(ctx => { + const parentLogger = ctx.container.get(ILogger); + return parentLogger.child('llm-communication-recorder'); + }).inSingletonScope().whenTargetNamed('llm-communication-recorder'); + + bindViewContribution(bind, AIHistoryViewContribution); + + bind(AIHistoryView).toSelf().inSingletonScope(); + bind(WidgetFactory).toDynamicValue(context => ({ + id: AIHistoryView.ID, + createWidget: () => context.container.get(AIHistoryView) + })).inSingletonScope(); + bind(TabBarToolbarContribution).toService(AIHistoryViewContribution); + +}); diff --git a/packages/ai-history/src/browser/ai-history-widget.tsx b/packages/ai-history/src/browser/ai-history-widget.tsx new file mode 100644 index 0000000000000..938b92f5e1ee2 --- /dev/null +++ b/packages/ai-history/src/browser/ai-history-widget.tsx @@ -0,0 +1,141 @@ +// ***************************************************************************** +// 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 { Agent, AgentService, CommunicationRecordingService, CommunicationRequestEntry, CommunicationResponseEntry } from '@theia/ai-core'; +import { codicon, ReactWidget, StatefulWidget } from '@theia/core/lib/browser'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; +import { CommunicationCard } from './ai-history-communication-card'; +import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component'; +import { deepClone, Emitter } from '@theia/core'; + +namespace AIHistoryView { + export interface State { + chronological: boolean; + } +} + +@injectable() +export class AIHistoryView extends ReactWidget implements StatefulWidget { + @inject(CommunicationRecordingService) + protected recordingService: CommunicationRecordingService; + @inject(AgentService) + protected readonly agentService: AgentService; + + public static ID = 'ai-history-widget'; + static LABEL = '✨ AI Agent History [Experimental]'; + + protected selectedAgent?: Agent; + + protected _state: AIHistoryView.State = { chronological: false }; + protected readonly onStateChangedEmitter = new Emitter(); + readonly onStateChanged = this.onStateChangedEmitter.event; + + constructor() { + super(); + this.id = AIHistoryView.ID; + this.title.label = AIHistoryView.LABEL; + this.title.caption = AIHistoryView.LABEL; + this.title.closable = true; + this.title.iconClass = codicon('history'); + } + + protected get state(): AIHistoryView.State { + return this._state; + } + + protected set state(state: AIHistoryView.State) { + this._state = state; + this.onStateChangedEmitter.fire(this._state); + } + + storeState(): object { + return this.state; + } + + restoreState(oldState: object & Partial): void { + const copy = deepClone(this.state); + if (oldState.chronological) { + copy.chronological = oldState.chronological; + } + this.state = copy; + } + + @postConstruct() + protected init(): void { + this.update(); + this.toDispose.push(this.recordingService.onDidRecordRequest(entry => this.historyContentUpdated(entry))); + this.toDispose.push(this.recordingService.onDidRecordResponse(entry => this.historyContentUpdated(entry))); + this.toDispose.push(this.recordingService.onStructuralChange(() => this.update())); + this.toDispose.push(this.onStateChanged(newState => this.update())); + this.selectAgent(this.agentService.getAllAgents()[0]); + } + + protected selectAgent(agent: Agent | undefined): void { + this.selectedAgent = agent; + this.update(); + } + + protected historyContentUpdated(entry: CommunicationRequestEntry | CommunicationResponseEntry): void { + if (entry.agentId === this.selectedAgent?.id) { + this.update(); + } + } + + render(): React.ReactNode { + const selectionChange = (value: SelectOption) => { + this.selectedAgent = this.agentService.getAllAgents().find(agent => agent.id === value.value); + this.update(); + }; + return ( +
    + ({ value: agent.id, label: agent.name, description: agent.description }))} + onChange={selectionChange} + defaultValue={this.selectedAgent?.id} /> +
    + {this.renderHistory()} +
    +
    + ); + } + + protected renderHistory(): React.ReactNode { + if (!this.selectedAgent) { + return
    No agent selected.
    ; + } + const history = [...this.recordingService.getHistory(this.selectedAgent.id)]; + if (history.length === 0) { + return
    No history available for the selected agent '{this.selectedAgent.name}'.
    ; + } + if (!this.state.chronological) { + history.reverse(); + } + return history.map(entry => ); + } + + protected onClick(e: React.MouseEvent, agent: Agent): void { + e.stopPropagation(); + this.selectAgent(agent); + } + + public sortHistory(chronological: boolean): void { + this.state = { ...deepClone(this.state), chronological: chronological }; + } + + get isChronological(): boolean { + return this.state.chronological === true; + } +} diff --git a/packages/ai-history/src/browser/style/ai-history.css b/packages/ai-history/src/browser/style/ai-history.css new file mode 100644 index 0000000000000..0b494ea1f4498 --- /dev/null +++ b/packages/ai-history/src/browser/style/ai-history.css @@ -0,0 +1,75 @@ +.agent-history-widget { + display: flex; + flex-direction: column; + align-items: center; +} + + +.agent-history-widget .theia-select-component { + margin: 10px 0; + width: 80%; +} + +.agent-history { + width: calc(80% + 16px); + display: flex; + align-items: center; + flex-direction: column; +} + +.theia-card { + background-color: var(--theia-sideBar-background); + border: 1px solid var(--theia-sideBarSectionHeader-border); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 15px; + margin: 10px 0; + width: 100%; + box-sizing: border-box; +} + +.theia-card-meta { + display: flex; + justify-content: space-between; + font-size: 0.9em; + margin-bottom: var(--theia-ui-padding); + padding: var(--theia-ui-padding) 0; +} + +.theia-card-content { + color: var(--theia-font-color); + margin-bottom: 10px; +} + +.theia-card-content p { + margin: var(--theia-ui-padding) 0; +} + +.theia-card-request, .theia-card-response { + margin-bottom: 10px; +} + +.theia-card-request pre, +.theia-card-response pre { + font-family: monospace; + white-space: pre-wrap; + word-wrap: break-word; + background-color: var(--theia-sideBar-background); + margin: var(--theia-ui-padding) 0; +} + +.theia-card-request-id, +.theia-card-session-id, +.theia-card-timestamp, +.theia-card-response-time { + flex: 1; +} + +.theia-card-request-id, +.theia-card-timestamp { + text-align: left; +} + +.theia-card-session-id, +.theia-card-response-time { + text-align: right; +} diff --git a/packages/ai-history/src/common/communication-recording-service.spec.ts b/packages/ai-history/src/common/communication-recording-service.spec.ts new file mode 100644 index 0000000000000..662d2d58c2706 --- /dev/null +++ b/packages/ai-history/src/common/communication-recording-service.spec.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// 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 { ILogger } from '@theia/core'; +import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; +import { DefaultCommunicationRecordingService } from './communication-recording-service'; +import { expect } from 'chai'; + +describe('DefaultCommunicationRecordingService', () => { + + it('records history', () => { + const service = new DefaultCommunicationRecordingService(); + (service as unknown as { logger: ILogger }).logger = new MockLogger(); + service.recordRequest({ agentId: 'agent', requestId: '1', sessionId: '1', timestamp: 100, request: 'dummy request' }); + + const history1 = service.getHistory('agent'); + expect(history1[0].request).to.eq('dummy request'); + + service.recordResponse({ agentId: 'agent', requestId: '1', sessionId: '1', timestamp: 200, response: 'dummy response' }); + const history2 = service.getHistory('agent'); + expect(history2[0].request).to.eq('dummy request'); + expect(history2[0].response).to.eq('dummy response'); + }); + +}); diff --git a/packages/ai-history/src/common/communication-recording-service.ts b/packages/ai-history/src/common/communication-recording-service.ts new file mode 100644 index 0000000000000..d32eb6ffc9121 --- /dev/null +++ b/packages/ai-history/src/common/communication-recording-service.ts @@ -0,0 +1,71 @@ +// ***************************************************************************** +// 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 { CommunicationHistory, CommunicationHistoryEntry, CommunicationRecordingService, CommunicationRequestEntry, CommunicationResponseEntry } from '@theia/ai-core'; +import { Emitter, Event, ILogger } from '@theia/core'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; + +@injectable() +export class DefaultCommunicationRecordingService implements CommunicationRecordingService { + + @inject(ILogger) @named('llm-communication-recorder') + protected logger: ILogger; + + protected onDidRecordRequestEmitter = new Emitter(); + readonly onDidRecordRequest: Event = this.onDidRecordRequestEmitter.event; + + protected onDidRecordResponseEmitter = new Emitter(); + readonly onDidRecordResponse: Event = this.onDidRecordResponseEmitter.event; + + protected onStructuralChangeEmitter = new Emitter(); + readonly onStructuralChange: Event = this.onStructuralChangeEmitter.event; + + protected history: Map = new Map(); + + getHistory(agentId: string): CommunicationHistory { + return this.history.get(agentId) || []; + } + + recordRequest(requestEntry: CommunicationHistoryEntry): void { + this.logger.debug('Recording request:', requestEntry.request); + if (this.history.has(requestEntry.agentId)) { + this.history.get(requestEntry.agentId)?.push(requestEntry); + } else { + this.history.set(requestEntry.agentId, [requestEntry]); + } + this.onDidRecordRequestEmitter.fire(requestEntry); + } + + recordResponse(responseEntry: CommunicationHistoryEntry): void { + this.logger.debug('Recording response:', responseEntry.response); + if (this.history.has(responseEntry.agentId)) { + const entry = this.history.get(responseEntry.agentId); + if (entry) { + const matchingRequest = entry.find(e => e.requestId === responseEntry.requestId); + if (!matchingRequest) { + throw Error('No matching request found for response'); + } + matchingRequest.response = responseEntry.response; + matchingRequest.responseTime = responseEntry.timestamp - matchingRequest.timestamp; + this.onDidRecordResponseEmitter.fire(responseEntry); + } + } + } + + clearHistory(): void { + this.history.clear(); + this.onStructuralChangeEmitter.fire(undefined); + } +} diff --git a/packages/ai-history/src/common/index.ts b/packages/ai-history/src/common/index.ts new file mode 100644 index 0000000000000..52a9128e1cb3f --- /dev/null +++ b/packages/ai-history/src/common/index.ts @@ -0,0 +1,17 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export * from './communication-recording-service'; diff --git a/packages/ai-history/tsconfig.json b/packages/ai-history/tsconfig.json new file mode 100644 index 0000000000000..548b369565b41 --- /dev/null +++ b/packages/ai-history/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../filesystem" + }, + { + "path": "../output" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/ai-llamafile/.eslintrc.js b/packages/ai-llamafile/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-llamafile/.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-llamafile/README.md b/packages/ai-llamafile/README.md new file mode 100644 index 0000000000000..17a4dc5464a8e --- /dev/null +++ b/packages/ai-llamafile/README.md @@ -0,0 +1,57 @@ +# AI Llamafile Integration + +The AI Llamafile package provides an integration that allows users to manage and interact with Llamafile language models within Theia IDE. + +## Features + +- Start and stop Llamafile language servers. + +## Commands + +### Start Llamafile + +- **Command ID:** `llamafile.start` +- **Label:** `Start Llamafile` +- **Functionality:** Allows you to start a Llamafile language server by selecting from a list of configured Llamafiles. + +### Stop Llamafile + +- **Command ID:** `llamafile.stop` +- **Label:** `Stop Llamafile` +- **Functionality:** Allows you to stop a running Llamafile language server by selecting from a list of currently running Llamafiles. + +## Usage + +1. **Starting a Llamafile Language Server:** + + - Use the command palette to invoke `Start Llamafile`. + - A quick pick menu will appear with a list of configured Llamafiles. + - Select a Llamafile to start its language server. + +2. **Stopping a Llamafile Language Server:** + - Use the command palette to invoke `Stop Llamafile`. + - A quick pick menu will display a list of currently running Llamafiles. + - Select a Llamafile to stop its language server. + +## Dependencies + +This extension depends on the `@theia/ai-core` package for AI-related services and functionalities. + +## Configuration + +Make sure to configure your Llamafiles properly within the preference settings. +This setting is an array of objects, where each object defines a llamafile with a user-friendly name, the file uri, and the port to start the server on. + +Example Configuration: + +```json +{ + "ai-features.llamafile.llamafiles": [ + { + "name": "MyLlamaFile", + "uri": "file:///path/to/my.llamafile", + "port": 30000 + } + ] +} +``` diff --git a/packages/ai-llamafile/package.json b/packages/ai-llamafile/package.json new file mode 100644 index 0000000000000..9518a24ad7bd6 --- /dev/null +++ b/packages/ai-llamafile/package.json @@ -0,0 +1,50 @@ +{ + "name": "@theia/ai-llamafile", + "version": "1.54.0", + "description": "Theia - Llamafile Integration", + "dependencies": { + "@theia/ai-core": "1.54.0", + "@theia/core": "1.54.0", + "@theia/output": "1.54.0", + "tslib": "^2.6.2" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/llamafile-frontend-module", + "backend": "lib/node/llamafile-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" + ], + "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-llamafile/src/browser/llamafile-command-contribution.ts b/packages/ai-llamafile/src/browser/llamafile-command-contribution.ts new file mode 100644 index 0000000000000..eae616cfd94bd --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-command-contribution.ts @@ -0,0 +1,92 @@ +// ***************************************************************************** +// 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 { AICommandHandlerFactory } from '@theia/ai-core/lib/browser/ai-command-handler-factory'; +import { CommandContribution, CommandRegistry, MessageService } from '@theia/core'; +import { PreferenceService, QuickInputService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { LlamafileEntry, LlamafileManager } from '../common/llamafile-manager'; +import { PREFERENCE_LLAMAFILE } from './llamafile-preferences'; + +export const StartLlamafileCommand = { + id: 'llamafile.start', + label: 'Start Llamafile', +}; +export const StopLlamafileCommand = { + id: 'llamafile.stop', + label: 'Stop Llamafile', +}; + +@injectable() +export class LlamafileCommandContribution implements CommandContribution { + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(AICommandHandlerFactory) + protected readonly commandHandlerFactory: AICommandHandlerFactory; + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(MessageService) + protected messageService: MessageService; + + @inject(LlamafileManager) + protected llamafileManager: LlamafileManager; + + registerCommands(commandRegistry: CommandRegistry): void { + commandRegistry.registerCommand(StartLlamafileCommand, this.commandHandlerFactory({ + execute: async () => { + try { + const llamaFiles = this.preferenceService.get(PREFERENCE_LLAMAFILE); + if (llamaFiles === undefined || llamaFiles.length === 0) { + this.messageService.error('No Llamafiles configured.'); + return; + } + const options = llamaFiles.map(llamaFile => ({ label: llamaFile.name })); + const result = await this.quickInputService.showQuickPick(options); + if (result === undefined) { + return; + } + this.llamafileManager.startServer(result.label); + } catch (error) { + console.error('Something went wrong during the llamafile start.', error); + this.messageService.error(`Something went wrong during the llamafile start: ${error.message}.\nFor more information, see the console.`); + } + } + })); + commandRegistry.registerCommand(StopLlamafileCommand, this.commandHandlerFactory({ + execute: async () => { + try { + const llamaFiles = await this.llamafileManager.getStartedLlamafiles(); + if (llamaFiles === undefined || llamaFiles.length === 0) { + this.messageService.error('No Llamafiles running.'); + return; + } + const options = llamaFiles.map(llamaFile => ({ label: llamaFile })); + const result = await this.quickInputService.showQuickPick(options); + if (result === undefined) { + return; + } + this.llamafileManager.stopServer(result.label); + } catch (error) { + console.error('Something went wrong during the llamafile stop.', error); + this.messageService.error(`Something went wrong during the llamafile stop: ${error.message}.\nFor more information, see the console.`); + } + } + })); + } +} diff --git a/packages/ai-llamafile/src/browser/llamafile-frontend-application-contribution.ts b/packages/ai-llamafile/src/browser/llamafile-frontend-application-contribution.ts new file mode 100644 index 0000000000000..c202ca12fe54e --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-frontend-application-contribution.ts @@ -0,0 +1,59 @@ +// ***************************************************************************** +// 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 { FrontendApplicationContribution, PreferenceService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { LlamafileEntry, LlamafileManager } from '../common/llamafile-manager'; +import { PREFERENCE_LLAMAFILE } from './llamafile-preferences'; + +@injectable() +export class LlamafileFrontendApplicationContribution implements FrontendApplicationContribution { + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(LlamafileManager) + protected llamafileManager: LlamafileManager; + + private _knownLlamaFiles: Map = new Map(); + + onStart(): void { + this.preferenceService.ready.then(() => { + const llamafiles = this.preferenceService.get(PREFERENCE_LLAMAFILE, []); + this.llamafileManager.addLanguageModels(llamafiles); + llamafiles.forEach(model => this._knownLlamaFiles.set(model.name, model)); + + this.preferenceService.onPreferenceChanged(event => { + if (event.preferenceName === PREFERENCE_LLAMAFILE) { + // only new models which are actual LLamaFileEntries + const newModels = event.newValue.filter((llamafileEntry: unknown) => LlamafileEntry.is(llamafileEntry)) as LlamafileEntry[]; + + const llamafilesToAdd = newModels.filter(llamafile => + !this._knownLlamaFiles.has(llamafile.name) || !LlamafileEntry.equals(this._knownLlamaFiles.get(llamafile.name)!, llamafile)); + + const llamafileIdsToRemove = [...this._knownLlamaFiles.values()].filter(llamafile => + !newModels.find(a => LlamafileEntry.equals(a, llamafile))).map(a => a.name); + + this.llamafileManager.removeLanguageModels(llamafileIdsToRemove); + llamafileIdsToRemove.forEach(model => this._knownLlamaFiles.delete(model)); + + this.llamafileManager.addLanguageModels(llamafilesToAdd); + llamafilesToAdd.forEach(model => this._knownLlamaFiles.set(model.name, model)); + } + }); + }); + } +} diff --git a/packages/ai-llamafile/src/browser/llamafile-frontend-module.ts b/packages/ai-llamafile/src/browser/llamafile-frontend-module.ts new file mode 100644 index 0000000000000..5deeb0c18d982 --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-frontend-module.ts @@ -0,0 +1,45 @@ +// ***************************************************************************** +// 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 { CommandContribution } from '@theia/core'; +import { FrontendApplicationContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { OutputChannelManager, OutputChannelSeverity } from '@theia/output/lib/browser/output-channel'; +import { LlamafileManager, LlamafileManagerPath, LlamafileServerManagerClient } from '../common/llamafile-manager'; +import { LlamafileCommandContribution } from './llamafile-command-contribution'; +import { LlamafileFrontendApplicationContribution } from './llamafile-frontend-application-contribution'; +import { bindAILlamafilePreferences } from './llamafile-preferences'; + +export default new ContainerModule(bind => { + bind(FrontendApplicationContribution).to(LlamafileFrontendApplicationContribution).inSingletonScope(); + bind(CommandContribution).to(LlamafileCommandContribution).inSingletonScope(); + bind(LlamafileManager).toDynamicValue(ctx => { + const connection = ctx.container.get(RemoteConnectionProvider); + const outputChannelManager = ctx.container.get(OutputChannelManager); + const client: LlamafileServerManagerClient = { + error: (llamafileName, message) => { + const channel = outputChannelManager.getChannel(`${llamafileName}-llamafile`); + channel.appendLine(message, OutputChannelSeverity.Error); + }, + log: (llamafileName, message) => { + const channel = outputChannelManager.getChannel(`${llamafileName}-llamafile`); + channel.appendLine(message, OutputChannelSeverity.Info); + } + }; + return connection.createProxy(LlamafileManagerPath, client); + }).inSingletonScope(); + + bindAILlamafilePreferences(bind); +}); diff --git a/packages/ai-llamafile/src/browser/llamafile-preferences.ts b/packages/ai-llamafile/src/browser/llamafile-preferences.ts new file mode 100644 index 0000000000000..fcc3255725ab7 --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-preferences.ts @@ -0,0 +1,60 @@ +// ***************************************************************************** +// 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 { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser'; +import { interfaces } from '@theia/core/shared/inversify'; + +export const AI_LLAMAFILE_PREFERENCES_TITLE = '✨ AI LlamaFile'; +export const PREFERENCE_LLAMAFILE = 'ai-features.llamafile.llamafiles'; + +export const aiLlamafilePreferencesSchema: PreferenceSchema = { + type: 'object', + properties: { + [PREFERENCE_LLAMAFILE]: { + title: AI_LLAMAFILE_PREFERENCES_TITLE, + markdownDescription: '❗ This setting allows you to add llamafiles.\ + \n\ + You need to provide a user friendly `name`, the file `uri` to the llamafile and the `port` to use.\ + \n\ + In order to start your llamafile you have to call the "Start Llamafile" command where you can then select the llamafile to start.\ + \n\ + If you modify an entry, e.g. change the port and the server was already running, then it will be stopped and you have to manually start it again.', + type: 'array', + default: [], + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'The model name to use for this Llamafile.' + }, + uri: { + type: 'string', + description: 'The file uri to the Llamafile.' + }, + port: { + type: 'number', + description: 'The port to use to start the server.' + } + } + } + } + } +}; + +export function bindAILlamafilePreferences(bind: interfaces.Bind): void { + bind(PreferenceContribution).toConstantValue({ schema: aiLlamafilePreferencesSchema }); +} diff --git a/packages/ai-llamafile/src/common/llamafile-language-model.ts b/packages/ai-llamafile/src/common/llamafile-language-model.ts new file mode 100644 index 0000000000000..6f1b039013879 --- /dev/null +++ b/packages/ai-llamafile/src/common/llamafile-language-model.ts @@ -0,0 +1,102 @@ +// ***************************************************************************** +// 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 { LanguageModel, LanguageModelRequest, LanguageModelResponse, LanguageModelStreamResponsePart } from '@theia/ai-core'; + +export class LlamafileLanguageModel implements LanguageModel { + + readonly providerId = 'llamafile'; + readonly vendor: string = 'Mozilla'; + + constructor(readonly name: string, readonly uri: string, readonly port: number) { + } + + get id(): string { + return this.name; + } + + async request(request: LanguageModelRequest): Promise { + try { + let prompt = request.messages.map(message => { + switch (message.actor) { + case 'user': + return `User: ${message.query}`; + case 'ai': + return `Llama: ${message.query}`; + case 'system': + return `${message.query.replace(/\n\n/g, '\n')}`; + } + }).join('\n'); + prompt += '\nLlama:'; + const response = await fetch(`http://localhost:${this.port}/completion`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prompt: prompt, + n_predict: 200, + stream: true, + stop: ['', 'Llama:', 'User:', '<|eot_id|>'], + cache_prompt: true, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (!response.body) { + throw new Error('Response body is undefined'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + return { + stream: { + [Symbol.asyncIterator](): AsyncIterator { + return { + async next(): Promise> { + const { value, done } = await reader.read(); + if (done) { + return { value: undefined, done: true }; + } + const read = decoder.decode(value, { stream: true }); + const chunk = read.split('\n').filter(l => l.length !== 0).reduce((acc, line) => { + try { + const parsed = JSON.parse(line.substring(6)); + acc += parsed.content; + return acc; + } catch (error) { + console.error('Error parsing JSON:', error); + return acc; + } + }, ''); + return { value: { content: chunk }, done: false }; + } + }; + } + } + }; + } catch (error) { + console.error('Error:', error); + return { + text: `Error: ${error}` + }; + } + } + +} diff --git a/packages/ai-llamafile/src/common/llamafile-manager.ts b/packages/ai-llamafile/src/common/llamafile-manager.ts new file mode 100644 index 0000000000000..561f3acdee95b --- /dev/null +++ b/packages/ai-llamafile/src/common/llamafile-manager.ts @@ -0,0 +1,50 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +export const LlamafileManager = Symbol('LlamafileManager'); + +export const LlamafileManagerPath = '/services/llamafilemanager'; + +export interface LlamafileManager { + startServer(name: string): Promise; + stopServer(name: string): void; + getStartedLlamafiles(): Promise; + setClient(client: LlamafileServerManagerClient): void; + addLanguageModels(llamaFiles: LlamafileEntry[]): Promise; + removeLanguageModels(modelIds: string[]): void; +} +export interface LlamafileServerManagerClient { + log(llamafileName: string, message: string): void; + error(llamafileName: string, message: string): void; +} + +export interface LlamafileEntry { + name: string; + uri: string; + port: number; +} + +export namespace LlamafileEntry { + export function equals(a: LlamafileEntry, b: LlamafileEntry): boolean { + return a.name === b.name && a.uri === b.uri && a.port === b.port; + } + export function is(entry: unknown): entry is LlamafileEntry { + // eslint-disable-next-line no-null/no-null + return typeof entry === 'object' && entry !== null + && 'name' in entry && typeof entry.name === 'string' + && 'uri' in entry && typeof entry.uri === 'string' + && 'port' in entry && typeof entry.port === 'number'; + } +} diff --git a/packages/ai-llamafile/src/node/llamafile-backend-module.ts b/packages/ai-llamafile/src/node/llamafile-backend-module.ts new file mode 100644 index 0000000000000..83ab2c182bd00 --- /dev/null +++ b/packages/ai-llamafile/src/node/llamafile-backend-module.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { LlamafileManagerImpl } from './llamafile-manager-impl'; +import { LlamafileManager, LlamafileServerManagerClient, LlamafileManagerPath } from '../common/llamafile-manager'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; + +export default new ContainerModule(bind => { + bind(LlamafileManager).to(LlamafileManagerImpl).inSingletonScope(); + bind(ConnectionHandler).toDynamicValue(ctx => new RpcConnectionHandler( + LlamafileManagerPath, + client => { + const service = ctx.container.get(LlamafileManager); + service.setClient(client); + return service; + } + )).inSingletonScope(); +}); diff --git a/packages/ai-llamafile/src/node/llamafile-manager-impl.ts b/packages/ai-llamafile/src/node/llamafile-manager-impl.ts new file mode 100644 index 0000000000000..3d726457f9faa --- /dev/null +++ b/packages/ai-llamafile/src/node/llamafile-manager-impl.ts @@ -0,0 +1,109 @@ +// ***************************************************************************** +// 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 { LanguageModelRegistry } from '@theia/ai-core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; +import { basename, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { LlamafileLanguageModel } from '../common/llamafile-language-model'; +import { LlamafileEntry, LlamafileManager, LlamafileServerManagerClient } from '../common/llamafile-manager'; + +@injectable() +export class LlamafileManagerImpl implements LlamafileManager { + + @inject(LanguageModelRegistry) + protected languageModelRegistry: LanguageModelRegistry; + + private processMap: Map = new Map(); + private client: LlamafileServerManagerClient; + + async addLanguageModels(llamaFiles: LlamafileEntry[]): Promise { + for (const llamafile of llamaFiles) { + const model = await this.languageModelRegistry.getLanguageModel(llamafile.name); + if (model) { + if (!(model instanceof LlamafileLanguageModel)) { + console.warn(`Llamafile: model ${model.id} is not an LLamafile model`); + continue; + } else { + // This can happen during the initializing of more than one frontends, changes are handled in the frontend + console.info(`Llamafile: skip creating or updating model ${llamafile.name} because it already exists.`); + } + } else { + this.languageModelRegistry.addLanguageModels([new LlamafileLanguageModel(llamafile.name, llamafile.uri, llamafile.port)]); + } + } + } + removeLanguageModels(modelIds: string[]): void { + modelIds.filter(modelId => this.isStarted(modelId)).forEach(modelId => this.stopServer(modelId)); + this.languageModelRegistry.removeLanguageModels(modelIds); + } + + async getStartedLlamafiles(): Promise { + const models = await this.languageModelRegistry.getLanguageModels(); + return models.filter(model => model instanceof LlamafileLanguageModel && this.isStarted(model.name)).map(model => model.id); + } + + async startServer(name: string): Promise { + if (!this.processMap.has(name)) { + const models = await this.languageModelRegistry.getLanguageModels(); + const llm = models.find(model => model.id === name && model instanceof LlamafileLanguageModel) as LlamafileLanguageModel | undefined; + if (llm === undefined) { + return Promise.reject(`Llamafile ${name} not found`); + } + const filePath = fileURLToPath(llm.uri); + + // Extract the directory and file name + const dir = dirname(filePath); + const fileName = basename(filePath); + const currentProcess = spawn(`./${fileName}`, ['--port', '' + llm.port, '--server', '--nobrowser'], { cwd: dir }); + this.processMap.set(name, currentProcess); + + currentProcess.stdout.on('data', (data: Buffer) => { + const output = data.toString(); + this.client.log(name, output); + }); + currentProcess.stderr.on('data', (data: Buffer) => { + const output = data.toString(); + this.client.error(name, output); + }); + currentProcess.on('close', code => { + this.client.log(name, `LlamaFile process for file ${name} exited with code ${code}`); + this.processMap.delete(name); + }); + currentProcess.on('error', error => { + this.client.error(name, `Error starting LlamaFile process for file ${name}: ${error.message}`); + this.processMap.delete(name); + }); + } + } + + stopServer(name: string): void { + if (this.processMap.has(name)) { + const currentProcess = this.processMap.get(name); + currentProcess!.kill(); + this.processMap.delete(name); + } + } + + isStarted(name: string): boolean { + return this.processMap.has(name); + } + + setClient(client: LlamafileServerManagerClient): void { + this.client = client; + } + +} diff --git a/packages/ai-llamafile/src/package.spec.ts b/packages/ai-llamafile/src/package.spec.ts new file mode 100644 index 0000000000000..ac0ffad7fa139 --- /dev/null +++ b/packages/ai-llamafile/src/package.spec.ts @@ -0,0 +1,27 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('ai-llamafile package', () => { + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-llamafile/tsconfig.json b/packages/ai-llamafile/tsconfig.json new file mode 100644 index 0000000000000..ed8ef9826cf57 --- /dev/null +++ b/packages/ai-llamafile/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../output" + } + ] +} diff --git a/packages/ai-ollama/.eslintrc.js b/packages/ai-ollama/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-ollama/.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-ollama/README.md b/packages/ai-ollama/README.md new file mode 100644 index 0000000000000..f9eba7a29e173 --- /dev/null +++ b/packages/ai-ollama/README.md @@ -0,0 +1,30 @@ +
    + +
    + +theia-ext-logo + +

    ECLIPSE THEIA - Ollama EXTENSION

    + +
    + +
    + +## Description + +The `@theia/ai-ollama` integrates Ollama's models with Theia AI. + +## 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 + diff --git a/packages/ai-ollama/package.json b/packages/ai-ollama/package.json new file mode 100644 index 0000000000000..b2a5d74346abb --- /dev/null +++ b/packages/ai-ollama/package.json @@ -0,0 +1,53 @@ +{ + "name": "@theia/ai-ollama", + "version": "1.54.0", + "description": "Theia - Ollama Integration", + "dependencies": { + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/workspace": "1.54.0", + "minimatch": "^5.1.0", + "tslib": "^2.6.2", + "ollama": "^0.5.8", + "@theia/ai-core": "1.54.0" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/ollama-frontend-module", + "backend": "lib/node/ollama-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" + ], + "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-ollama/src/browser/ollama-frontend-application-contribution.ts b/packages/ai-ollama/src/browser/ollama-frontend-application-contribution.ts new file mode 100644 index 0000000000000..e08680563cd72 --- /dev/null +++ b/packages/ai-ollama/src/browser/ollama-frontend-application-contribution.ts @@ -0,0 +1,59 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox 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 { FrontendApplicationContribution, PreferenceService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { OllamaLanguageModelsManager } from '../common'; +import { HOST_PREF, MODELS_PREF } from './ollama-preferences'; + +@injectable() +export class OllamaFrontendApplicationContribution implements FrontendApplicationContribution { + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(OllamaLanguageModelsManager) + protected manager: OllamaLanguageModelsManager; + + protected prevModels: string[] = []; + + onStart(): void { + this.preferenceService.ready.then(() => { + const host = this.preferenceService.get(HOST_PREF, 'http://localhost:11434'); + this.manager.setHost(host); + + const models = this.preferenceService.get(MODELS_PREF, []); + this.manager.createLanguageModels(...models); + this.prevModels = [...models]; + + this.preferenceService.onPreferenceChanged(event => { + if (event.preferenceName === HOST_PREF) { + this.manager.setHost(event.newValue); + } else if (event.preferenceName === MODELS_PREF) { + const oldModels = new Set(this.prevModels); + const newModels = new Set(event.newValue as string[]); + + const modelsToRemove = [...oldModels].filter(model => !newModels.has(model)); + const modelsToAdd = [...newModels].filter(model => !oldModels.has(model)); + + this.manager.removeLanguageModels(...modelsToRemove); + this.manager.createLanguageModels(...modelsToAdd); + this.prevModels = [...event.newValue]; + } + }); + }); + } +} diff --git a/packages/ai-ollama/src/browser/ollama-frontend-module.ts b/packages/ai-ollama/src/browser/ollama-frontend-module.ts new file mode 100644 index 0000000000000..7228aba4b4332 --- /dev/null +++ b/packages/ai-ollama/src/browser/ollama-frontend-module.ts @@ -0,0 +1,31 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { OllamaPreferencesSchema } from './ollama-preferences'; +import { FrontendApplicationContribution, PreferenceContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser'; +import { OllamaFrontendApplicationContribution } from './ollama-frontend-application-contribution'; +import { OLLAMA_LANGUAGE_MODELS_MANAGER_PATH, OllamaLanguageModelsManager } from '../common'; + +export default new ContainerModule(bind => { + bind(PreferenceContribution).toConstantValue({ schema: OllamaPreferencesSchema }); + bind(OllamaFrontendApplicationContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(OllamaFrontendApplicationContribution); + bind(OllamaLanguageModelsManager).toDynamicValue(ctx => { + const provider = ctx.container.get(RemoteConnectionProvider); + return provider.createProxy(OLLAMA_LANGUAGE_MODELS_MANAGER_PATH); + }).inSingletonScope(); +}); diff --git a/packages/ai-ollama/src/browser/ollama-preferences.ts b/packages/ai-ollama/src/browser/ollama-preferences.ts new file mode 100644 index 0000000000000..40036d9fa5e7c --- /dev/null +++ b/packages/ai-ollama/src/browser/ollama-preferences.ts @@ -0,0 +1,40 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox 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 { PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; +import { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/browser/ai-core-preferences'; + +export const HOST_PREF = 'ai-features.ollama.ollamaHost'; +export const MODELS_PREF = 'ai-features.ollama.ollamaModels'; + +export const OllamaPreferencesSchema: PreferenceSchema = { + type: 'object', + properties: { + [HOST_PREF]: { + type: 'string', + title: AI_CORE_PREFERENCES_TITLE, + default: 'http://localhost:11434' + }, + [MODELS_PREF]: { + type: 'array', + title: AI_CORE_PREFERENCES_TITLE, + default: ['llama3', 'gemma2'], + items: { + type: 'string' + } + } + } +}; diff --git a/packages/ai-ollama/src/common/index.ts b/packages/ai-ollama/src/common/index.ts new file mode 100644 index 0000000000000..8d6821f9cc79e --- /dev/null +++ b/packages/ai-ollama/src/common/index.ts @@ -0,0 +1,16 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox 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 +// ***************************************************************************** +export * from './ollama-language-models-manager'; diff --git a/packages/ai-ollama/src/common/ollama-language-models-manager.ts b/packages/ai-ollama/src/common/ollama-language-models-manager.ts new file mode 100644 index 0000000000000..2714ef3a7757a --- /dev/null +++ b/packages/ai-ollama/src/common/ollama-language-models-manager.ts @@ -0,0 +1,23 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox 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 +// ***************************************************************************** +export const OLLAMA_LANGUAGE_MODELS_MANAGER_PATH = '/services/ollama/language-model-manager'; +export const OllamaLanguageModelsManager = Symbol('OllamaLanguageModelsManager'); +export interface OllamaLanguageModelsManager { + host: string | undefined; + setHost(host: string | undefined): void; + createLanguageModels(...modelIds: string[]): Promise; + removeLanguageModels(...modelIds: string[]): void +} diff --git a/packages/ai-ollama/src/node/ollama-backend-module.ts b/packages/ai-ollama/src/node/ollama-backend-module.ts new file mode 100644 index 0000000000000..667729c7fc4e2 --- /dev/null +++ b/packages/ai-ollama/src/node/ollama-backend-module.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { OLLAMA_LANGUAGE_MODELS_MANAGER_PATH, OllamaLanguageModelsManager } from '../common/ollama-language-models-manager'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; +import { OllamaLanguageModelsManagerImpl } from './ollama-language-models-manager-impl'; + +export const OllamaModelFactory = Symbol('OllamaModelFactory'); + +export default new ContainerModule(bind => { + bind(OllamaLanguageModelsManagerImpl).toSelf().inSingletonScope(); + bind(OllamaLanguageModelsManager).toService(OllamaLanguageModelsManagerImpl); + bind(ConnectionHandler).toDynamicValue(ctx => + new RpcConnectionHandler(OLLAMA_LANGUAGE_MODELS_MANAGER_PATH, () => ctx.container.get(OllamaLanguageModelsManager)) + ).inSingletonScope(); +}); diff --git a/packages/ai-ollama/src/node/ollama-language-model.ts b/packages/ai-ollama/src/node/ollama-language-model.ts new file mode 100644 index 0000000000000..899305831f7b5 --- /dev/null +++ b/packages/ai-ollama/src/node/ollama-language-model.ts @@ -0,0 +1,155 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox 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 { + LanguageModel, + LanguageModelParsedResponse, + LanguageModelRequest, + LanguageModelRequestMessage, + LanguageModelResponse, + LanguageModelStreamResponsePart, + ToolRequest +} from '@theia/ai-core'; +import { CancellationToken } from '@theia/core'; +import { ChatRequest, ChatResponse, Message, Ollama, Tool } from 'ollama'; + +export const OllamaModelIdentifier = Symbol('OllamaModelIdentifier'); + +export class OllamaModel implements LanguageModel { + + protected readonly DEFAULT_REQUEST_SETTINGS: Partial> = { + keep_alive: '15m' + }; + + readonly providerId = 'ollama'; + readonly vendor: string = 'Ollama'; + + constructor(protected readonly model: string, protected host: () => string | undefined) { + } + + get id(): string { + return this.providerId + '/' + this.model; + } + + get name(): string { + return this.model; + } + + async request(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise { + const ollama = this.initializeOllama(); + + if (request.response_format?.type === 'json_schema') { + return this.handleStructuredOutputRequest(ollama, request); + } + const response = await ollama.chat({ + ...this.DEFAULT_REQUEST_SETTINGS, + model: this.model, + messages: request.messages.map(this.toOllamaMessage), + stream: true, + tools: request.tools?.map(this.toOllamaTool), + ...request.settings + }); + + cancellationToken?.onCancellationRequested(() => { + response.abort(); + }); + + async function* wrapAsyncIterator(inputIterable: AsyncIterable): AsyncIterable { + for await (const item of inputIterable) { + // TODO handle tool calls + yield { content: item.message.content }; + } + } + return { stream: wrapAsyncIterator(response) }; + } + + protected async handleStructuredOutputRequest(ollama: Ollama, request: LanguageModelRequest): Promise { + const result = await ollama.chat({ + ...this.DEFAULT_REQUEST_SETTINGS, + model: this.model, + messages: request.messages.map(this.toOllamaMessage), + format: 'json', + ...request.settings + }); + try { + return { + content: result.message.content, + parsed: JSON.parse(result.message.content) + }; + } catch (error) { + // TODO use ILogger + console.log('Failed to parse structured response from the language model.', error); + return { + content: result.message.content, + parsed: {} + }; + } + } + + protected initializeOllama(): Ollama { + const host = this.host(); + if (!host) { + throw new Error('Please provide OLLAMA_HOST in preferences or via environment variable'); + } + return new Ollama({ host: host }); + } + + protected toOllamaTool(tool: ToolRequest): Tool { + const transform = (props: Record | undefined) => { + if (!props) { + return undefined; + } + const result: Record = {}; + for (const key in props) { + if (Object.prototype.hasOwnProperty.call(props, key)) { + result[key] = { + type: props[key].type, + description: key + }; + } + } + return result; + }; + return { + type: 'function', + function: { + name: tool.name, + description: tool.description ?? 'Tool named ' + tool.name, + parameters: { + type: tool.parameters?.type ?? 'object', + required: Object.keys(tool.parameters?.properties ?? {}), + properties: transform(tool.parameters?.properties) ?? {} + }, + } + }; + } + + protected toOllamaMessage(message: LanguageModelRequestMessage): Message { + if (message.actor === 'ai') { + return { role: 'assistant', content: message.query || '' }; + } + if (message.actor === 'user') { + return { role: 'user', content: message.query || '' }; + } + if (message.actor === 'system') { + return { role: 'system', content: message.query || '' }; + } + return { role: 'system', content: '' }; + } +} diff --git a/packages/ai-ollama/src/node/ollama-language-models-manager-impl.ts b/packages/ai-ollama/src/node/ollama-language-models-manager-impl.ts new file mode 100644 index 0000000000000..1fbd1f520c3c8 --- /dev/null +++ b/packages/ai-ollama/src/node/ollama-language-models-manager-impl.ts @@ -0,0 +1,58 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox 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 { LanguageModelRegistry } from '@theia/ai-core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { OllamaModel } from './ollama-language-model'; +import { OllamaLanguageModelsManager } from '../common'; + +@injectable() +export class OllamaLanguageModelsManagerImpl implements OllamaLanguageModelsManager { + + protected _host: string | undefined; + + @inject(LanguageModelRegistry) + protected readonly languageModelRegistry: LanguageModelRegistry; + + get host(): string | undefined { + return this._host ?? process.env.OLLAMA_HOST; + } + + // Triggered from frontend. In case you want to use the models on the backend + // without a frontend then call this yourself + async createLanguageModels(...modelIds: string[]): Promise { + for (const id of modelIds) { + // TODO check that the model exists in Ollama using `list`. Ask and trigger download if not. + if (!(await this.languageModelRegistry.getLanguageModel(`ollama/${id}`))) { + this.languageModelRegistry.addLanguageModels([new OllamaModel(id, () => this.host)]); + } else { + console.info(`Ollama: skip creating model ${id} because it already exists`); + } + } + } + + removeLanguageModels(...modelIds: string[]): void { + this.languageModelRegistry.removeLanguageModels(modelIds.map(id => `ollama/${id}`)); + } + + setHost(host: string | undefined): void { + if (host) { + this._host = host; + } else { + this._host = undefined; + } + } +} diff --git a/packages/ai-ollama/src/package.spec.ts b/packages/ai-ollama/src/package.spec.ts new file mode 100644 index 0000000000000..2b813c28eef54 --- /dev/null +++ b/packages/ai-ollama/src/package.spec.ts @@ -0,0 +1,27 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('ai-ollama package', () => { + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-ollama/tsconfig.json b/packages/ai-ollama/tsconfig.json new file mode 100644 index 0000000000000..61a997fc14fd1 --- /dev/null +++ b/packages/ai-ollama/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../filesystem" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/ai-openai/.eslintrc.js b/packages/ai-openai/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-openai/.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-openai/README.md b/packages/ai-openai/README.md new file mode 100644 index 0000000000000..1ec7facb4a1b0 --- /dev/null +++ b/packages/ai-openai/README.md @@ -0,0 +1,49 @@ +
    + +
    + +theia-ext-logo + +

    ECLIPSE THEIA - Open AI EXTENSION

    + +
    + +
    + +## Description + +The `@theia/ai-openai` integrates OpenAI's models with Theia AI. +The OpenAI API key and the models to use can be configured via preferences. +Alternatively the OpenAI API key can also be handed in via the `OPENAI_API_KEY` variable. + +### Custom models + +The extension also supports OpenAI compatible models hosted on different end points. +You can configure the end points via the `ai-features.openAiCustom.customOpenAiModels` preference: + +```ts +{ + model: string + url: string + id?: string + apiKey?: string | true +} +``` + +- `model` and `url` are mandatory attributes, indicating the end point and model to use +- `id` is an optional attribute which is used in the UI to refer to this configuration +- `apiKey` is either the key to access the API served at the given URL or `true` to use the global OpenAI API key. If not given 'no-key' will be used. + +## 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-openai/package.json b/packages/ai-openai/package.json new file mode 100644 index 0000000000000..3ac4dd9ab057e --- /dev/null +++ b/packages/ai-openai/package.json @@ -0,0 +1,53 @@ +{ + "name": "@theia/ai-openai", + "version": "1.54.0", + "description": "Theia - OpenAI Integration", + "dependencies": { + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/workspace": "1.54.0", + "minimatch": "^5.1.0", + "tslib": "^2.6.2", + "openai": "^4.55.7", + "@theia/ai-core": "1.54.0" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/openai-frontend-module", + "backend": "lib/node/openai-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" + ], + "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-openai/src/browser/openai-frontend-application-contribution.ts b/packages/ai-openai/src/browser/openai-frontend-application-contribution.ts new file mode 100644 index 0000000000000..b16f80aa0ef90 --- /dev/null +++ b/packages/ai-openai/src/browser/openai-frontend-application-contribution.ts @@ -0,0 +1,100 @@ +// ***************************************************************************** +// 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 { FrontendApplicationContribution, PreferenceService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { OpenAiLanguageModelsManager, OpenAiModelDescription } from '../common'; +import { API_KEY_PREF, CUSTOM_ENDPOINTS_PREF, MODELS_PREF } from './openai-preferences'; + +@injectable() +export class OpenAiFrontendApplicationContribution implements FrontendApplicationContribution { + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(OpenAiLanguageModelsManager) + protected manager: OpenAiLanguageModelsManager; + + // The preferenceChange.oldValue is always undefined for some reason + protected prevModels: string[] = []; + protected prevCustomModels: Partial[] = []; + + onStart(): void { + this.preferenceService.ready.then(() => { + const apiKey = this.preferenceService.get(API_KEY_PREF, undefined); + this.manager.setApiKey(apiKey); + + const models = this.preferenceService.get(MODELS_PREF, []); + this.manager.createOrUpdateLanguageModels(...models.map(createOpenAIModelDescription)); + this.prevModels = [...models]; + + const customModels = this.preferenceService.get[]>(CUSTOM_ENDPOINTS_PREF, []); + this.manager.createOrUpdateLanguageModels(...createCustomModelDescriptionsFromPreferences(customModels)); + this.prevCustomModels = [...customModels]; + + this.preferenceService.onPreferenceChanged(event => { + if (event.preferenceName === API_KEY_PREF) { + this.manager.setApiKey(event.newValue); + } else if (event.preferenceName === MODELS_PREF) { + const oldModels = new Set(this.prevModels); + const newModels = new Set(event.newValue as string[]); + + const modelsToRemove = [...oldModels].filter(model => !newModels.has(model)); + const modelsToAdd = [...newModels].filter(model => !oldModels.has(model)); + + this.manager.removeLanguageModels(...modelsToRemove.map(model => `openai/${model}`)); + this.manager.createOrUpdateLanguageModels(...modelsToAdd.map(createOpenAIModelDescription)); + this.prevModels = [...event.newValue]; + } else if (event.preferenceName === CUSTOM_ENDPOINTS_PREF) { + const oldModels = createCustomModelDescriptionsFromPreferences(this.prevCustomModels); + const newModels = createCustomModelDescriptionsFromPreferences(event.newValue); + + const modelsToRemove = oldModels.filter(model => !newModels.some(newModel => newModel.id === model.id)); + const modelsToAddOrUpdate = newModels.filter(newModel => !oldModels.some(model => + model.id === newModel.id && model.model === newModel.model && model.url === newModel.url && model.apiKey === newModel.apiKey)); + + this.manager.removeLanguageModels(...modelsToRemove.map(model => model.id)); + this.manager.createOrUpdateLanguageModels(...modelsToAddOrUpdate); + } + }); + }); + } +} + +function createOpenAIModelDescription(modelId: string): OpenAiModelDescription { + return { + id: `openai/${modelId}`, + model: modelId, + apiKey: true + }; +} + +function createCustomModelDescriptionsFromPreferences(preferences: Partial[]): OpenAiModelDescription[] { + return preferences.reduce((acc, pref) => { + if (!pref.model || !pref.url || typeof pref.model !== 'string' || typeof pref.url !== 'string') { + return acc; + } + return [ + ...acc, + { + id: pref.id && typeof pref.id === 'string' ? pref.id : pref.model, + model: pref.model, + url: pref.url, + apiKey: typeof pref.apiKey === 'string' || pref.apiKey === true ? pref.apiKey : undefined + } + ]; + }, []); +} diff --git a/packages/ai-openai/src/browser/openai-frontend-module.ts b/packages/ai-openai/src/browser/openai-frontend-module.ts new file mode 100644 index 0000000000000..21ba05b95d7cd --- /dev/null +++ b/packages/ai-openai/src/browser/openai-frontend-module.ts @@ -0,0 +1,31 @@ +// ***************************************************************************** +// 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { OpenAiPreferencesSchema } from './openai-preferences'; +import { FrontendApplicationContribution, PreferenceContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser'; +import { OpenAiFrontendApplicationContribution } from './openai-frontend-application-contribution'; +import { OPENAI_LANGUAGE_MODELS_MANAGER_PATH, OpenAiLanguageModelsManager } from '../common'; + +export default new ContainerModule(bind => { + bind(PreferenceContribution).toConstantValue({ schema: OpenAiPreferencesSchema }); + bind(OpenAiFrontendApplicationContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(OpenAiFrontendApplicationContribution); + bind(OpenAiLanguageModelsManager).toDynamicValue(ctx => { + const provider = ctx.container.get(RemoteConnectionProvider); + return provider.createProxy(OPENAI_LANGUAGE_MODELS_MANAGER_PATH); + }).inSingletonScope(); +}); diff --git a/packages/ai-openai/src/browser/openai-preferences.ts b/packages/ai-openai/src/browser/openai-preferences.ts new file mode 100644 index 0000000000000..ce937578ce623 --- /dev/null +++ b/packages/ai-openai/src/browser/openai-preferences.ts @@ -0,0 +1,76 @@ +// ***************************************************************************** +// 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 { PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; +import { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/browser/ai-core-preferences'; + +export const API_KEY_PREF = 'ai-features.openAiOfficial.openAiApiKey'; +export const MODELS_PREF = 'ai-features.openAiOfficial.officialOpenAiModels'; +export const CUSTOM_ENDPOINTS_PREF = 'ai-features.openAiCustom.customOpenAiModels'; + +export const OpenAiPreferencesSchema: PreferenceSchema = { + type: 'object', + properties: { + [API_KEY_PREF]: { + type: 'string', + markdownDescription: 'Enter an API Key of your official OpenAI Account. **Please note:** By using this preference the Open AI API key will be stored in clear text\ + on the machine running Theia. Use the environment variable `OPENAI_API_KEY` to set the key securely.', + title: AI_CORE_PREFERENCES_TITLE, + }, + [MODELS_PREF]: { + type: 'array', + description: 'Official OpenAI models to use', + title: AI_CORE_PREFERENCES_TITLE, + default: ['gpt-4o', 'gpt-4o-2024-08-06', 'gpt-4o-2024-05-13', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'], + items: { + type: 'string' + } + }, + [CUSTOM_ENDPOINTS_PREF]: { + type: 'array', + title: AI_CORE_PREFERENCES_TITLE, + markdownDescription: 'Integrate custom models compatible with the OpenAI API, for example via `vllm`. The required attributes are `model` and `url`.\ + \n\ + Optionally, you can\ + \n\ + - specify a unique `id` to identify the custom model in the UI. If none is given `model` will be used as `id`.\ + \n\ + - provide an `apiKey` to access the API served at the given url. Use `true` to indicate the use of the global OpenAI API key.', + default: [], + items: { + type: 'object', + properties: { + model: { + type: 'string', + title: 'Model ID' + }, + url: { + type: 'string', + title: 'The Open AI API compatible endpoint where the model is hosted' + }, + id: { + type: 'string', + title: 'A unique identifier which is used in the UI to identify the custom model', + }, + apiKey: { + type: ['string', 'boolean'], + title: 'Either the key to access the API served at the given url or `true` to use the global OpenAI API key', + }, + } + } + } + } +}; diff --git a/packages/ai-openai/src/common/index.ts b/packages/ai-openai/src/common/index.ts new file mode 100644 index 0000000000000..d79fbf6c3872b --- /dev/null +++ b/packages/ai-openai/src/common/index.ts @@ -0,0 +1,16 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +export * from './openai-language-models-manager'; diff --git a/packages/ai-openai/src/common/openai-language-models-manager.ts b/packages/ai-openai/src/common/openai-language-models-manager.ts new file mode 100644 index 0000000000000..363b2552dc38b --- /dev/null +++ b/packages/ai-openai/src/common/openai-language-models-manager.ts @@ -0,0 +1,41 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +export const OPENAI_LANGUAGE_MODELS_MANAGER_PATH = '/services/open-ai/language-model-manager'; +export const OpenAiLanguageModelsManager = Symbol('OpenAiLanguageModelsManager'); +export interface OpenAiModelDescription { + /** + * The identifier of the model which will be shown in the UI. + */ + id: string; + /** + * The model ID as used by the OpenAI API. + */ + model: string; + /** + * The OpenAI API compatible endpoint where the model is hosted. If not provided the default OpenAI endpoint will be used. + */ + url?: string; + /** + * The key for the model. If 'true' is provided the global OpenAI API key will be used. + */ + apiKey: string | true | undefined; +} +export interface OpenAiLanguageModelsManager { + apiKey: string | undefined; + setApiKey(key: string | undefined): void; + createOrUpdateLanguageModels(...models: OpenAiModelDescription[]): Promise; + removeLanguageModels(...modelIds: string[]): void +} diff --git a/packages/ai-openai/src/node/openai-backend-module.ts b/packages/ai-openai/src/node/openai-backend-module.ts new file mode 100644 index 0000000000000..311065f402cc3 --- /dev/null +++ b/packages/ai-openai/src/node/openai-backend-module.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { OPENAI_LANGUAGE_MODELS_MANAGER_PATH, OpenAiLanguageModelsManager } from '../common/openai-language-models-manager'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; +import { OpenAiLanguageModelsManagerImpl } from './openai-language-models-manager-impl'; + +export const OpenAiModelFactory = Symbol('OpenAiModelFactory'); + +export default new ContainerModule(bind => { + bind(OpenAiLanguageModelsManagerImpl).toSelf().inSingletonScope(); + bind(OpenAiLanguageModelsManager).toService(OpenAiLanguageModelsManagerImpl); + bind(ConnectionHandler).toDynamicValue(ctx => + new RpcConnectionHandler(OPENAI_LANGUAGE_MODELS_MANAGER_PATH, () => ctx.container.get(OpenAiLanguageModelsManager)) + ).inSingletonScope(); +}); diff --git a/packages/ai-openai/src/node/openai-language-model.ts b/packages/ai-openai/src/node/openai-language-model.ts new file mode 100644 index 0000000000000..7d47f72d99584 --- /dev/null +++ b/packages/ai-openai/src/node/openai-language-model.ts @@ -0,0 +1,190 @@ +// ***************************************************************************** +// 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 { + LanguageModel, + LanguageModelParsedResponse, + LanguageModelRequest, + LanguageModelRequestMessage, + LanguageModelResponse, + LanguageModelStreamResponsePart +} from '@theia/ai-core'; +import { CancellationToken } from '@theia/core'; +import OpenAI from 'openai'; +import { ChatCompletionStream } from 'openai/lib/ChatCompletionStream'; +import { RunnableToolFunctionWithoutParse } from 'openai/lib/RunnableFunction'; +import { ChatCompletionMessageParam } from 'openai/resources'; + +export const OpenAiModelIdentifier = Symbol('OpenAiModelIdentifier'); + +function toOpenAIMessage(message: LanguageModelRequestMessage): ChatCompletionMessageParam { + return { + role: toOpenAiRole(message), + content: message.query || '' + }; +} + +function toOpenAiRole(message: LanguageModelRequestMessage): 'system' | 'user' | 'assistant' { + switch (message.actor) { + case 'system': + return 'system'; + case 'ai': + return 'assistant'; + default: + return 'user'; + } +} + +export class OpenAiModel implements LanguageModel { + + /** + * @param id the unique id for this language model. It will be used to identify the model in the UI. + * @param model the model id as it is used by the OpenAI API + * @param openAIInitializer initializer for the OpenAI client, used for each request. + */ + constructor(public readonly id: string, public model: string, public apiKey: () => string | undefined, public url: string | undefined) { } + + async request(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise { + const openai = this.initializeOpenAi(); + + if (request.response_format?.type === 'json_schema' && this.supportsStructuredOutput()) { + return this.handleStructuredOutputRequest(openai, request); + } + + let runner: ChatCompletionStream; + const tools = this.createTools(request); + if (tools) { + runner = openai.beta.chat.completions.runTools({ + model: this.model, + messages: request.messages.map(toOpenAIMessage), + stream: true, + tools: tools, + tool_choice: 'auto', + ...request.settings + }); + } else { + runner = openai.beta.chat.completions.stream({ + model: this.model, + messages: request.messages.map(toOpenAIMessage), + stream: true, + ...request.settings + }); + } + cancellationToken?.onCancellationRequested(() => { + runner.abort(); + }); + + let runnerEnd = false; + + let resolve: (part: LanguageModelStreamResponsePart) => void; + runner.on('error', error => { + console.error('Error in OpenAI chat completion stream:', error); + runnerEnd = true; + resolve({ content: error.message }); + }); + // we need to also listen for the emitted errors, as otherwise any error actually thrown by the API will not be caught + runner.emitted('error').then(error => { + console.error('Error in OpenAI chat completion stream:', error); + runnerEnd = true; + resolve({ content: error.message }); + }); + runner.emitted('abort').then(() => { + // do nothing, as the abort event is only emitted when the runner is aborted by us + }); + runner.on('message', message => { + if (message.role === 'tool') { + resolve({ tool_calls: [{ id: message.tool_call_id, finished: true, result: this.getCompletionContent(message) }] }); + } + console.debug('Received Open AI message', JSON.stringify(message)); + }); + runner.once('end', () => { + runnerEnd = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve(runner.finalChatCompletion as any); + }); + const asyncIterator = { + async *[Symbol.asyncIterator](): AsyncIterator { + runner.on('chunk', chunk => { + if (chunk.choices[0]?.delta) { + resolve({ ...chunk.choices[0]?.delta }); + } + }); + while (!runnerEnd) { + const promise = new Promise((res, rej) => { + resolve = res; + }); + yield promise; + } + } + }; + return { stream: asyncIterator }; + } + + protected supportsStructuredOutput(): boolean { + // see https://platform.openai.com/docs/models/gpt-4o + return [ + 'gpt-4o', + 'gpt-4o-2024-08-06', + 'gpt-4o-mini' + ].includes(this.model); + } + + protected async handleStructuredOutputRequest(openai: OpenAI, request: LanguageModelRequest): Promise { + // TODO implement tool support for structured output (parse() seems to require different tool format) + const result = await openai.beta.chat.completions.parse({ + model: this.model, + messages: request.messages.map(toOpenAIMessage), + response_format: request.response_format, + ...request.settings + }); + const message = result.choices[0].message; + if (message.refusal || message.parsed === undefined) { + console.error('Error in OpenAI chat completion stream:', JSON.stringify(message)); + } + return { + content: message.content ?? '', + parsed: message.parsed + }; + } + + private getCompletionContent(message: OpenAI.Chat.Completions.ChatCompletionToolMessageParam): string { + if (Array.isArray(message.content)) { + return message.content.join(''); + } + return message.content; + } + + protected createTools(request: LanguageModelRequest): RunnableToolFunctionWithoutParse[] | undefined { + return request.tools?.map(tool => ({ + type: 'function', + function: { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + function: (args_string: string) => tool.handler(args_string) + } + } as RunnableToolFunctionWithoutParse)); + } + + protected initializeOpenAi(): OpenAI { + const apiKey = this.apiKey(); + if (!apiKey && !(this.url)) { + throw new Error('Please provide OPENAI_API_KEY in preferences or via environment variable'); + } + // We need to hand over "some" key, even if a custom url is not key protected as otherwise the OpenAI client will throw an error + return new OpenAI({ apiKey: apiKey ?? 'no-key', baseURL: this.url }); + } +} diff --git a/packages/ai-openai/src/node/openai-language-models-manager-impl.ts b/packages/ai-openai/src/node/openai-language-models-manager-impl.ts new file mode 100644 index 0000000000000..cfc81ba3b8adb --- /dev/null +++ b/packages/ai-openai/src/node/openai-language-models-manager-impl.ts @@ -0,0 +1,78 @@ +// ***************************************************************************** +// 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 { LanguageModelRegistry } from '@theia/ai-core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { OpenAiModel } from './openai-language-model'; +import { OpenAiLanguageModelsManager, OpenAiModelDescription } from '../common'; + +@injectable() +export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsManager { + + protected _apiKey: string | undefined; + + @inject(LanguageModelRegistry) + protected readonly languageModelRegistry: LanguageModelRegistry; + + get apiKey(): string | undefined { + return this._apiKey ?? process.env.OPENAI_API_KEY; + } + + // Triggered from frontend. In case you want to use the models on the backend + // without a frontend then call this yourself + async createOrUpdateLanguageModels(...modelDescriptions: OpenAiModelDescription[]): Promise { + for (const modelDescription of modelDescriptions) { + const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id); + const apiKeyProvider = () => { + if (modelDescription.apiKey === true) { + return this.apiKey; + } + if (modelDescription.apiKey) { + return modelDescription.apiKey; + } + return undefined; + }; + if (model) { + if (!(model instanceof OpenAiModel)) { + console.warn(`Open AI: model ${modelDescription.id} is not an OpenAI model`); + continue; + } + if (!modelDescription.url) { + // This seems to be an official model, but it was already created. This can happen during the initializing of more than one frontend. + console.info(`Open AI: skip creating model ${modelDescription.id} because it already exists`); + continue; + } + model.url = modelDescription.url; + model.model = modelDescription.model; + model.apiKey = apiKeyProvider; + } else { + this.languageModelRegistry.addLanguageModels([new OpenAiModel(modelDescription.id, modelDescription.model, apiKeyProvider, modelDescription.url)]); + } + } + } + + removeLanguageModels(...modelIds: string[]): void { + this.languageModelRegistry.removeLanguageModels(modelIds); + } + + setApiKey(apiKey: string | undefined): void { + if (apiKey) { + this._apiKey = apiKey; + } else { + this._apiKey = undefined; + } + } +} diff --git a/packages/ai-openai/src/package.spec.ts b/packages/ai-openai/src/package.spec.ts new file mode 100644 index 0000000000000..7aa1df47bcb00 --- /dev/null +++ b/packages/ai-openai/src/package.spec.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource 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 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('ai-openai package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-openai/tsconfig.json b/packages/ai-openai/tsconfig.json new file mode 100644 index 0000000000000..61a997fc14fd1 --- /dev/null +++ b/packages/ai-openai/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../filesystem" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/ai-terminal/.eslintrc.js b/packages/ai-terminal/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-terminal/.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-terminal/README.md b/packages/ai-terminal/README.md new file mode 100644 index 0000000000000..9d172389e7b14 --- /dev/null +++ b/packages/ai-terminal/README.md @@ -0,0 +1,31 @@ +
    + +
    + +theia-ext-logo + +

    ECLIPSE THEIA - AI Terminal EXTENSION

    + +
    + +
    + +## Description + +The `@theia/ai-terminal` extension contributes an overlay to the terminal view.\ +The overlay can be used to ask a dedicated `TerminalAgent` for suggestions of terminal commands. + +## 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-terminal/package.json b/packages/ai-terminal/package.json new file mode 100644 index 0000000000000..acf37e6632656 --- /dev/null +++ b/packages/ai-terminal/package.json @@ -0,0 +1,51 @@ +{ + "name": "@theia/ai-terminal", + "version": "1.54.0", + "description": "Theia - AI Terminal Extension", + "dependencies": { + "@theia/core": "1.54.0", + "@theia/ai-core": "1.54.0", + "@theia/ai-chat": "1.54.0", + "@theia/terminal": "1.54.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.2" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/ai-terminal-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-terminal/src/browser/ai-terminal-agent.ts b/packages/ai-terminal/src/browser/ai-terminal-agent.ts new file mode 100644 index 0000000000000..98d083d7bee76 --- /dev/null +++ b/packages/ai-terminal/src/browser/ai-terminal-agent.ts @@ -0,0 +1,234 @@ +// ***************************************************************************** +// 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 { + Agent, + CommunicationRecordingService, + getJsonOfResponse, + isLanguageModelParsedResponse, + LanguageModelRegistry, LanguageModelRequirement, + PromptService +} from '@theia/ai-core/lib/common'; +import { generateUuid, ILogger } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +const Commands = z.object({ + commands: z.array(z.string()), +}); +type Commands = z.infer; + +@injectable() +export class AiTerminalAgent implements Agent { + @inject(CommunicationRecordingService) + protected recordingService: CommunicationRecordingService; + + id = 'Terminal Assistant'; + name = 'Terminal Assistant'; + description = 'This agent provides assistance to write and execute arbitrary terminal commands. \ + Based on the user\'s request, it suggests commands and allows the user to directly paste and execute them in the terminal. \ + It accesses the current directory, environment and the recent terminal output of the terminal session to provide context-aware assistance'; + variables = []; + functions = []; + agentSpecificVariables = [ + { name: 'userRequest', usedInPrompt: true, description: 'The user\'s question or request.' }, + { name: 'shell', usedInPrompt: true, description: 'The shell being used, e.g., /usr/bin/zsh.' }, + { name: 'cwd', usedInPrompt: true, description: 'The current working directory.' }, + { name: 'recentTerminalContents', usedInPrompt: true, description: 'The last 0 to 50 recent lines visible in the terminal.' } + ]; + promptTemplates = [ + { + id: 'terminal-system', + name: 'AI Terminal System Prompt', + description: 'Prompt for the AI Terminal Assistant', + template: ` +# Instructions +Generate one or more command suggestions based on the user's request, considering the shell being used, +the current working directory, and the recent terminal contents. Provide the best suggestion first, +followed by other relevant suggestions if the user asks for further options. + +Parameters: +- user-request: The user's question or request. +- shell: The shell being used, e.g., /usr/bin/zsh. +- cwd: The current working directory. +- recent-terminal-contents: The last 0 to 50 recent lines visible in the terminal. + +Return the result in the following JSON format: +{ + "commands": [ + "best_command_suggestion", + "next_best_command_suggestion", + "another_command_suggestion" + ] +} + +## Example +user-request: "How do I commit changes?" +shell: "/usr/bin/zsh" +cwd: "/home/user/project" +recent-terminal-contents: +git status +On branch main +Your branch is up to date with 'origin/main'. +nothing to commit, working tree clean + +## Expected JSON output +\`\`\`json +\{ + "commands": [ + "git commit", + "git commit --amend", + "git commit -a" + ] +} +\`\`\` +` + }, + { + id: 'terminal-user', + name: 'AI Terminal User Prompt', + description: 'Prompt that contains the user request', + template: ` +user-request: {{userRequest}} +shell: {{shell}} +cwd: {{cwd}} +recent-terminal-contents: +{{recentTerminalContents}} +` + } + ]; + languageModelRequirements: LanguageModelRequirement[] = [ + { + purpose: 'suggest-terminal-commands', + identifier: 'openai/gpt-4o', + } + ]; + + @inject(LanguageModelRegistry) + protected languageModelRegistry: LanguageModelRegistry; + + @inject(PromptService) + protected promptService: PromptService; + + @inject(ILogger) + protected logger: ILogger; + + async getCommands( + userRequest: string, + cwd: string, + shell: string, + recentTerminalContents: string[], + ): Promise { + const lm = await this.languageModelRegistry.selectLanguageModel({ + agent: this.id, + ...this.languageModelRequirements[0] + }); + if (!lm) { + this.logger.error('No language model available for the AI Terminal Agent.'); + return []; + } + + const parameters = { + userRequest, + shell, + cwd, + recentTerminalContents + }; + + const systemPrompt = await this.promptService.getPrompt('terminal-system', parameters).then(p => p?.text); + const userPrompt = await this.promptService.getPrompt('terminal-user', parameters).then(p => p?.text); + if (!systemPrompt || !userPrompt) { + this.logger.error('The prompt service didn\'t return prompts for the AI Terminal Agent.'); + return []; + } + + // since we do not actually hold complete conversions, the request/response pair is considered a session + const sessionId = generateUuid(); + const requestId = generateUuid(); + this.recordingService.recordRequest({ + agentId: this.id, + sessionId, + timestamp: Date.now(), + requestId, + request: systemPrompt, + messages: [userPrompt], + }); + + try { + const result = await lm.request({ + messages: [ + { + actor: 'ai', + type: 'text', + query: systemPrompt + }, + { + actor: 'user', + type: 'text', + query: userPrompt + } + ], + response_format: { + type: 'json_schema', + json_schema: { + name: 'terminal-commands', + description: 'Suggested terminal commands based on the user request', + schema: zodToJsonSchema(Commands) + } + } + }); + + if (isLanguageModelParsedResponse(result)) { + // model returned structured output + const parsedResult = Commands.safeParse(result.parsed); + if (parsedResult.success) { + const responseTextfromParsed = JSON.stringify(parsedResult.data.commands); + this.recordingService.recordResponse({ + agentId: this.id, + sessionId, + timestamp: Date.now(), + requestId, + response: responseTextfromParsed, + }); + return parsedResult.data.commands; + } + } + + // fall back to agent-based parsing of result + const jsonResult = await getJsonOfResponse(result); + const responseTextFromJSON = JSON.stringify(jsonResult); + this.recordingService.recordResponse({ + agentId: this.id, + sessionId, + timestamp: Date.now(), + requestId, + response: responseTextFromJSON + }); + const parsedJsonResult = Commands.safeParse(jsonResult); + if (parsedJsonResult.success) { + return parsedJsonResult.data.commands; + } + + return []; + + } catch (error) { + this.logger.error('Error obtaining the command suggestions.', error); + return []; + } + } + +} diff --git a/packages/ai-terminal/src/browser/ai-terminal-contribution.ts b/packages/ai-terminal/src/browser/ai-terminal-contribution.ts new file mode 100644 index 0000000000000..715932af62d62 --- /dev/null +++ b/packages/ai-terminal/src/browser/ai-terminal-contribution.ts @@ -0,0 +1,197 @@ +// ***************************************************************************** +// 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 { EXPERIMENTAL_AI_CONTEXT_KEY } from '@theia/ai-core/lib/browser'; +import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from '@theia/core'; +import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; +import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; +import { TerminalWidgetImpl } from '@theia/terminal/lib/browser/terminal-widget-impl'; +import { AiTerminalAgent } from './ai-terminal-agent'; +import { AICommandHandlerFactory } from '@theia/ai-core/lib/browser/ai-command-handler-factory'; +import { AgentService } from '@theia/ai-core'; + +const AI_TERMINAL_COMMAND = { + id: 'ai-terminal:open', + label: 'Ask the AI' +}; + +@injectable() +export class AiTerminalCommandContribution implements CommandContribution, MenuContribution, KeybindingContribution { + + @inject(TerminalService) + protected terminalService: TerminalService; + + @inject(AiTerminalAgent) + protected terminalAgent: AiTerminalAgent; + + @inject(AICommandHandlerFactory) + protected commandHandlerFactory: AICommandHandlerFactory; + + @inject(AgentService) + private readonly agentService: AgentService; + + registerKeybindings(keybindings: KeybindingRegistry): void { + keybindings.registerKeybinding({ + command: AI_TERMINAL_COMMAND.id, + keybinding: 'ctrlcmd+i', + when: `terminalFocus && ${EXPERIMENTAL_AI_CONTEXT_KEY}` + }); + } + registerMenus(menus: MenuModelRegistry): void { + menus.registerMenuAction([...TerminalMenus.TERMINAL_CONTEXT_MENU, '_5'], { + when: EXPERIMENTAL_AI_CONTEXT_KEY, + commandId: AI_TERMINAL_COMMAND.id + }); + } + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(AI_TERMINAL_COMMAND, this.commandHandlerFactory({ + execute: () => { + if (this.terminalService.currentTerminal instanceof TerminalWidgetImpl && this.agentService.isEnabled(this.terminalAgent.id)) { + new AiTerminalChatWidget( + this.terminalService.currentTerminal, + this.terminalAgent + ); + } + } + })); + } +} + +class AiTerminalChatWidget { + + protected chatContainer: HTMLDivElement; + protected chatInput: HTMLTextAreaElement; + protected chatResultParagraph: HTMLParagraphElement; + protected chatInputContainer: HTMLDivElement; + + protected haveResult = false; + commands: string[]; + + constructor( + protected terminalWidget: TerminalWidgetImpl, + protected terminalAgent: AiTerminalAgent + ) { + this.chatContainer = document.createElement('div'); + this.chatContainer.className = 'ai-terminal-chat-container'; + + const chatCloseButton = document.createElement('span'); + chatCloseButton.className = 'closeButton codicon codicon-close'; + chatCloseButton.onclick = () => this.dispose(); + this.chatContainer.appendChild(chatCloseButton); + + const chatResultContainer = document.createElement('div'); + chatResultContainer.className = 'ai-terminal-chat-result'; + this.chatResultParagraph = document.createElement('p'); + this.chatResultParagraph.textContent = 'How can I help you?'; + chatResultContainer.appendChild(this.chatResultParagraph); + this.chatContainer.appendChild(chatResultContainer); + + this.chatInputContainer = document.createElement('div'); + this.chatInputContainer.className = 'ai-terminal-chat-input-container'; + + this.chatInput = document.createElement('textarea'); + this.chatInput.className = 'theia-input theia-ChatInput'; + this.chatInput.placeholder = 'Ask about a terminal command...'; + this.chatInput.onkeydown = event => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + if (!this.haveResult) { + this.send(); + } else { + this.terminalWidget.sendText(this.chatResultParagraph.innerText); + this.dispose(); + } + } else if (event.key === 'Escape') { + this.dispose(); + } else if (event.key === 'ArrowUp' && this.haveResult) { + this.updateChatResult(this.getNextCommandIndex(1)); + } else if (event.key === 'ArrowDown' && this.haveResult) { + this.updateChatResult(this.getNextCommandIndex(-1)); + } + }; + this.chatInputContainer.appendChild(this.chatInput); + + const chatInputOptionsContainer = document.createElement('div'); + const chatInputOptionsSpan = document.createElement('span'); + chatInputOptionsSpan.className = 'codicon codicon-send option'; + chatInputOptionsSpan.title = 'Send'; + chatInputOptionsSpan.onclick = () => this.send(); + chatInputOptionsContainer.appendChild(chatInputOptionsSpan); + this.chatInputContainer.appendChild(chatInputOptionsContainer); + + this.chatContainer.appendChild(this.chatInputContainer); + + terminalWidget.node.appendChild(this.chatContainer); + + this.chatInput.focus(); + } + + protected async send(): Promise { + const userRequest = this.chatInput.value; + if (userRequest) { + this.chatInput.value = ''; + + this.chatResultParagraph.innerText = 'Loading'; + this.chatResultParagraph.className = 'loading'; + + const cwd = (await this.terminalWidget.cwd).toString(); + const processInfo = await this.terminalWidget.processInfo; + const shell = processInfo.executable; + const recentTerminalContents = this.getRecentTerminalCommands(); + + this.commands = await this.terminalAgent.getCommands(userRequest, cwd, shell, recentTerminalContents); + + if (this.commands.length > 0) { + this.chatResultParagraph.className = 'command'; + this.chatResultParagraph.innerText = this.commands[0]; + this.chatInput.placeholder = 'Hit enter to confirm'; + if (this.commands.length > 1) { + this.chatInput.placeholder += ' or use ⇅ to show alternatives...'; + } + this.haveResult = true; + } else { + this.chatResultParagraph.className = ''; + this.chatResultParagraph.innerText = 'No results'; + this.chatInput.placeholder = 'Try again...'; + } + } + } + + protected getRecentTerminalCommands(): string[] { + const maxLines = 100; + return this.terminalWidget.buffer.getLines(0, + this.terminalWidget.buffer.length > maxLines ? maxLines : this.terminalWidget.buffer.length + ); + } + + protected getNextCommandIndex(step: number): number { + const currentIndex = this.commands.indexOf(this.chatResultParagraph.innerText); + const nextIndex = (currentIndex + step + this.commands.length) % this.commands.length; + return nextIndex; + } + + protected updateChatResult(index: number): void { + this.chatResultParagraph.innerText = this.commands[index]; + } + + protected dispose(): void { + this.chatInput.value = ''; + this.terminalWidget.node.removeChild(this.chatContainer); + this.terminalWidget.getTerminal().focus(); + } +} diff --git a/packages/ai-terminal/src/browser/ai-terminal-frontend-module.ts b/packages/ai-terminal/src/browser/ai-terminal-frontend-module.ts new file mode 100644 index 0000000000000..9f8ff9c059540 --- /dev/null +++ b/packages/ai-terminal/src/browser/ai-terminal-frontend-module.ts @@ -0,0 +1,34 @@ +// ***************************************************************************** +// 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 { Agent } from '@theia/ai-core/lib/common'; +import { CommandContribution, MenuContribution } from '@theia/core'; +import { KeybindingContribution } from '@theia/core/lib/browser'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { AiTerminalAgent } from './ai-terminal-agent'; +import { AiTerminalCommandContribution } from './ai-terminal-contribution'; + +import '../../src/browser/style/ai-terminal.css'; + +export default new ContainerModule(bind => { + bind(AiTerminalCommandContribution).toSelf().inSingletonScope(); + for (const identifier of [CommandContribution, MenuContribution, KeybindingContribution]) { + bind(identifier).toService(AiTerminalCommandContribution); + } + + bind(AiTerminalAgent).toSelf().inSingletonScope(); + bind(Agent).toService(AiTerminalAgent); +}); diff --git a/packages/ai-terminal/src/browser/style/ai-terminal.css b/packages/ai-terminal/src/browser/style/ai-terminal.css new file mode 100644 index 0000000000000..acce0a411a725 --- /dev/null +++ b/packages/ai-terminal/src/browser/style/ai-terminal.css @@ -0,0 +1,94 @@ +.ai-terminal-chat-container { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: 500px; + padding: 10px; + box-sizing: border-box; + background: var(--theia-menu-background); + color: var(--theia-menu-foreground); + margin-bottom: 12px; + display: flex; + flex-direction: column; + align-items: center; + border: 1px solid var(--theia-menu-border); +} + +.ai-terminal-chat-container .closeButton { + position: absolute; + top: 1em; + right: 1em; + cursor: pointer; +} + +.ai-terminal-chat-container .closeButton:hover { + color: var(--theia-menu-foreground); +} + +.ai-terminal-chat-result { + width: 100%; + margin-bottom: 10px; +} + +.ai-terminal-chat-input-container { + width: 100%; + display: flex; + align-items: center; +} + +.ai-terminal-chat-input-container textarea { + flex-grow: 1; + height: 36px; + background-color: var(--theia-input-background); + border-radius: 4px; + box-sizing: border-box; + padding: 8px; + resize: none; + overflow: hidden; + line-height: 1.3rem; + margin-right: 10px; /* Add some space between textarea and button */ +} + +.ai-terminal-chat-input-container .option { + width: 21px; + height: 21px; + display: inline-block; + box-sizing: border-box; + user-select: none; + background-repeat: no-repeat; + background-position: center; + border: var(--theia-border-width) solid transparent; + opacity: 0.7; + cursor: pointer; +} + +.ai-terminal-chat-input-container .option:hover { + opacity: 1; +} + +@keyframes dots { + 0%, + 20% { + content: ""; + } + 40% { + content: "."; + } + 60% { + content: ".."; + } + 80%, + 100% { + content: "..."; + } +} +.ai-terminal-chat-result p.loading::after { + content: ""; + animation: dots 1s steps(1, end) infinite; +} + +.ai-terminal-chat-result p.command { + font-family: "Droid Sans Mono", "monospace", monospace; +} diff --git a/packages/ai-terminal/src/package.spec.ts b/packages/ai-terminal/src/package.spec.ts new file mode 100644 index 0000000000000..7c55c63eb414f --- /dev/null +++ b/packages/ai-terminal/src/package.spec.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource 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 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('ai-terminal package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-terminal/tsconfig.json b/packages/ai-terminal/tsconfig.json new file mode 100644 index 0000000000000..9269a0f774e34 --- /dev/null +++ b/packages/ai-terminal/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-chat" + }, + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../terminal" + } + ] +} diff --git a/packages/ai-workspace-agent/.eslintrc.js b/packages/ai-workspace-agent/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-workspace-agent/.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-workspace-agent/README.md b/packages/ai-workspace-agent/README.md new file mode 100644 index 0000000000000..947501725540d --- /dev/null +++ b/packages/ai-workspace-agent/README.md @@ -0,0 +1,30 @@ +
    + +
    + +theia-ext-logo + +

    ECLIPSE THEIA - AI Workspace Agent EXTENSION

    + +
    + +
    + +## Description + +The `@theia/ai-workspace-agent` extension contributes the `Workspace` agent to Theia AI. +The agent is able to inspect the current files of the workspace, including their content, to answer questions. + +## 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-workspace-agent/package.json b/packages/ai-workspace-agent/package.json new file mode 100644 index 0000000000000..d992fb740a4cc --- /dev/null +++ b/packages/ai-workspace-agent/package.json @@ -0,0 +1,53 @@ +{ + "name": "@theia/ai-workspace-agent", + "version": "1.54.0", + "description": "AI Workspace Agent 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", + "keywords": [ + "theia-extension" + ], + "dependencies": { + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/workspace": "1.54.0", + "@theia/navigator": "1.54.0", + "@theia/terminal": "1.54.0", + "@theia/ai-core": "1.54.0", + "@theia/ai-chat": "1.54.0" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@theia/cli": "1.54.0", + "@theia/test": "1.54.0" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/frontend-module" + } + ], + "files": [ + "lib", + "src" + ], + "scripts": { + "build": "theiaext build", + "clean": "theiaext clean", + "compile": "theiaext compile", + "lint": "theiaext lint", + "test": "theiaext test", + "watch": "theiaext watch" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/ai-workspace-agent/src/browser/frontend-module.ts b/packages/ai-workspace-agent/src/browser/frontend-module.ts new file mode 100644 index 0000000000000..101f3702b0cce --- /dev/null +++ b/packages/ai-workspace-agent/src/browser/frontend-module.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { ChatAgent } from '@theia/ai-chat/lib/common'; +import { Agent, ToolProvider } from '@theia/ai-core/lib/common'; +import { WorkspaceAgent } from './workspace-agent'; +import { FileContentFunction, GetWorkspaceFileList } from './functions'; + +export default new ContainerModule(bind => { + bind(WorkspaceAgent).toSelf().inSingletonScope(); + bind(Agent).toService(WorkspaceAgent); + bind(ChatAgent).toService(WorkspaceAgent); + bind(ToolProvider).to(GetWorkspaceFileList); + bind(ToolProvider).to(FileContentFunction); +}); diff --git a/packages/ai-workspace-agent/src/browser/functions.ts b/packages/ai-workspace-agent/src/browser/functions.ts new file mode 100644 index 0000000000000..d6b3a4fa6d68d --- /dev/null +++ b/packages/ai-workspace-agent/src/browser/functions.ts @@ -0,0 +1,134 @@ +// ***************************************************************************** +// 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 { ToolProvider, ToolRequest } from '@theia/ai-core'; +import { URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID } from '../common/functions'; + +/** + * A Function that can read the contents of a File from the Workspace. + */ +@injectable() +export class FileContentFunction implements ToolProvider { + static ID = FILE_CONTENT_FUNCTION_ID; + + getTool(): ToolRequest { + return { + id: FileContentFunction.ID, + name: FileContentFunction.ID, + description: 'Get the content of the file', + parameters: { + type: 'object', + properties: { + file: { + type: 'string', + description: 'The path of the file to retrieve content for', + } + } + }, + handler: (arg_string: string) => { + const file = this.parseArg(arg_string); + return this.getFileContent(file); + } + }; + } + + @inject(WorkspaceService) + protected workspaceService: WorkspaceService; + + @inject(FileService) + protected readonly fileService: FileService; + + private parseArg(arg_string: string): string { + const result = JSON.parse(arg_string); + return result.file; + } + + private async getFileContent(file: string): Promise { + const uri = new URI(file); + const fileContent = await this.fileService.read(uri); + return fileContent.value; + } +} + +/** + * A Function that lists all files in the workspace. + */ +@injectable() +export class GetWorkspaceFileList implements ToolProvider { + static ID = GET_WORKSPACE_FILE_LIST_FUNCTION_ID; + + getTool(): ToolRequest { + return { + id: GetWorkspaceFileList.ID, + name: GetWorkspaceFileList.ID, + description: 'List all files in the workspace', + + handler: () => this.getProjectFileList() + }; + } + + @inject(WorkspaceService) + protected workspaceService: WorkspaceService; + + @inject(FileService) + protected readonly fileService: FileService; + + async getProjectFileList(): Promise { + // Get all files from the workspace service as a flat list of qualified file names + const wsRoots = await this.workspaceService.roots; + const result: string[] = []; + for (const root of wsRoots) { + result.push(...await this.listFilesRecursively(root.resource)); + } + return result; + } + + private async listFilesRecursively(uri: URI): Promise { + const stat = await this.fileService.resolve(uri); + const result: string[] = []; + if (stat && stat.isDirectory) { + if (this.exclude(stat)) { + return result; + } + const children = await this.fileService.resolve(uri); + if (children.children) { + for (const child of children.children) { + result.push(child.resource.toString()); + result.push(...await this.listFilesRecursively(child.resource)); + } + } + } + return result; + } + + // Exclude folders which are not relevant to the AI Agent + private exclude(stat: FileStat): boolean { + if (stat.resource.path.base.startsWith('.')) { + return true; + } + if (stat.resource.path.base === 'node_modules') { + return true; + } + if (stat.resource.path.base === 'lib') { + return true; + } + return false; + } +} diff --git a/packages/ai-workspace-agent/src/browser/workspace-agent.ts b/packages/ai-workspace-agent/src/browser/workspace-agent.ts new file mode 100644 index 0000000000000..3d05487a16d9a --- /dev/null +++ b/packages/ai-workspace-agent/src/browser/workspace-agent.ts @@ -0,0 +1,53 @@ +// ***************************************************************************** +// 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 { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } from '@theia/ai-chat/lib/common'; +import { AgentSpecificVariables, PromptTemplate, ToolInvocationRegistry } from '@theia/ai-core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { workspaceTemplate } from '../common/template'; +import { FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID } from '../common/functions'; + +@injectable() +export class WorkspaceAgent extends AbstractStreamParsingChatAgent implements ChatAgent { + name: string; + description: string; + promptTemplates: PromptTemplate[]; + variables: never[]; + readonly agentSpecificVariables: AgentSpecificVariables[]; + readonly functions: string[]; + + @inject(ToolInvocationRegistry) + protected toolInvocationRegistry: ToolInvocationRegistry; + + constructor() { + super('Workspace', [{ + purpose: 'chat', + identifier: 'openai/gpt-4o', + }], 'chat'); + this.name = 'Workspace'; + this.description = 'This agent can access the users workspace, it can get a list of all available files and retrieve their content. \ + It can therefore answer questions about the current project, project files and source code in the workspace, such as how to build the project, \ + where to put source code, where to find specific code or configurations, etc.'; + this.promptTemplates = [workspaceTemplate]; + this.variables = []; + this.agentSpecificVariables = []; + this.functions = [GET_WORKSPACE_FILE_LIST_FUNCTION_ID, FILE_CONTENT_FUNCTION_ID]; + } + + protected override async getSystemMessageDescription(): Promise { + const resolvedPrompt = await this.promptService.getPrompt(workspaceTemplate.id); + return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined; + } +} diff --git a/packages/ai-workspace-agent/src/common/functions.ts b/packages/ai-workspace-agent/src/common/functions.ts new file mode 100644 index 0000000000000..852a6c8f60f95 --- /dev/null +++ b/packages/ai-workspace-agent/src/common/functions.ts @@ -0,0 +1,17 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +export const FILE_CONTENT_FUNCTION_ID = 'getFileContent'; +export const GET_WORKSPACE_FILE_LIST_FUNCTION_ID = 'getWorkspaceFileList'; diff --git a/packages/ai-workspace-agent/src/common/template.ts b/packages/ai-workspace-agent/src/common/template.ts new file mode 100644 index 0000000000000..ec825d90a98df --- /dev/null +++ b/packages/ai-workspace-agent/src/common/template.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 { PromptTemplate } from '@theia/ai-core/lib/common'; +import { GET_WORKSPACE_FILE_LIST_FUNCTION_ID, FILE_CONTENT_FUNCTION_ID } from './functions'; + +export const workspaceTemplate = { + id: 'workspace-system', + template: `# Instructions + + You are an AI assistant integrated into the Theia IDE, specifically designed to help software developers by +providing concise and accurate answers to programming-related questions. Your role is to enhance the +developer's productivity by offering quick solutions, explanations, and best practices. +Keep responses short and to the point, focusing on delivering valuable insights, best practices and +simple solutions. +You are specialized in providing insights based on the Theia IDE's workspace and its files. +Use the following functions to access the workspace: +- ~{${GET_WORKSPACE_FILE_LIST_FUNCTION_ID}} +- ~{${FILE_CONTENT_FUNCTION_ID}}. Never shorten the file paths when using this function. + +## Guidelines + +1. **Understand Context:** + - **Always answer in context of the workspace and its files. Avoid general answers**. + - Use the provided functions to access the workspace files. **Never assume the workspace structure or file contents.** + - Tailor responses to be relevant to the programming language, framework, or tools like Eclipse Theia used in the workspace. + - Ask clarifying questions if necessary to provide accurate assistance. Always assume it is okay to read additional files from the workspace. + +2. **Provide Clear Solutions:** + - Offer direct answers or code snippets that solve the problem or clarify the concept. + - Avoid lengthy explanations unless necessary for understanding. + - Provide links to official documentation for further reading when applicable. + +3. **Support Multiple Languages and Tools:** + - Be familiar with popular programming languages, frameworks, IDEs like Eclipse Theia, and command-line tools. + - Adapt advice based on the language, environment, or tools specified by the developer. + +4. **Facilitate Learning:** + - Encourage learning by explaining why a solution works or why a particular approach is recommended. + - Keep explanations concise and educational. + +5. **Maintain Professional Tone:** + - Communicate in a friendly, professional manner. + - Use technical jargon appropriately, ensuring clarity for the target audience. + +6. **Stay on Topic:** + - Limit responses strictly to topics related to software development, frameworks, Eclipse Theia, terminal usage, and relevant technologies. + - Politely decline to answer questions unrelated to these areas by saying, "I'm here to assist with programming-related questions. + For other topics, please refer to a specialized source." +` +}; diff --git a/packages/ai-workspace-agent/src/package.spec.ts b/packages/ai-workspace-agent/src/package.spec.ts new file mode 100644 index 0000000000000..106f1490b2d7a --- /dev/null +++ b/packages/ai-workspace-agent/src/package.spec.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource 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 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('ai-workspace-agent package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-workspace-agent/tsconfig.json b/packages/ai-workspace-agent/tsconfig.json new file mode 100644 index 0000000000000..60c1ac9586d07 --- /dev/null +++ b/packages/ai-workspace-agent/tsconfig.json @@ -0,0 +1,40 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../dev-packages/cli" + }, + { + "path": "../ai-chat" + }, + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../filesystem" + }, + { + "path": "../navigator" + }, + { + "path": "../terminal" + }, + { + "path": "../test" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/bulk-edit/package.json b/packages/bulk-edit/package.json index f231132ffe941..c0352dc098be6 100644 --- a/packages/bulk-edit/package.json +++ b/packages/bulk-edit/package.json @@ -1,14 +1,15 @@ { "name": "@theia/bulk-edit", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Bulk Edit Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/monaco": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/workspace": "1.44.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/workspace": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -43,7 +44,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/bulk-edit/src/browser/bulk-edit-tree/bulk-edit-tree.spec.ts b/packages/bulk-edit/src/browser/bulk-edit-tree/bulk-edit-tree.spec.ts index 49f38561ec75c..3f9e315579e7f 100644 --- a/packages/bulk-edit/src/browser/bulk-edit-tree/bulk-edit-tree.spec.ts +++ b/packages/bulk-edit/src/browser/bulk-edit-tree/bulk-edit-tree.spec.ts @@ -17,7 +17,7 @@ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; import * as chai from 'chai'; import { ResourceTextEdit } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService'; -import { URI as Uri } from 'vscode-uri'; +import { URI as Uri } from '@theia/core/shared/vscode-uri'; let disableJSDOM = enableJSDOM(); diff --git a/packages/callhierarchy/package.json b/packages/callhierarchy/package.json index 73eb81a3e448f..ebad7bf16c619 100644 --- a/packages/callhierarchy/package.json +++ b/packages/callhierarchy/package.json @@ -1,11 +1,12 @@ { "name": "@theia/callhierarchy", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Call Hierarchy Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "ts-md5": "^1.2.2" + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "ts-md5": "^1.2.2", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -40,7 +41,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/collaboration/.eslintrc.js b/packages/collaboration/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/collaboration/.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/collaboration/README.md b/packages/collaboration/README.md new file mode 100644 index 0000000000000..f97352acc298f --- /dev/null +++ b/packages/collaboration/README.md @@ -0,0 +1,33 @@ +
    + +
    + +theia-ext-logo + +

    ECLIPSE THEIA - COLLABORATION EXTENSION

    + +
    + +
    + +## Description + +The `@theia/collaboration` extension features to enable collaboration between multiple peers using Theia. +This is built on top of the [Open Collaboration Tools](https://www.open-collab.tools/) ([GitHub](https://github.com/TypeFox/open-collaboration-tools)) project. + +Note that the project is still in a beta phase and can be subject to unexpected breaking changes. This package is therefore in a beta phase as well. + +## Additional Information + +- [API documentation for `@theia/collaboration`](https://eclipse-theia.github.io/theia/docs/next/modules/collaboration.html) +- [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/collaboration/package.json b/packages/collaboration/package.json new file mode 100644 index 0000000000000..76a4a88e72794 --- /dev/null +++ b/packages/collaboration/package.json @@ -0,0 +1,57 @@ +{ + "name": "@theia/collaboration", + "version": "1.54.0", + "description": "Theia - Collaboration Extension", + "dependencies": { + "@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/workspace": "1.54.0", + "open-collaboration-protocol": "0.2.0", + "open-collaboration-yjs": "0.2.0", + "socket.io-client": "^4.5.3", + "yjs": "^13.6.7", + "lib0": "^0.2.52", + "y-protocols": "^1.0.6" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/collaboration-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/collaboration/src/browser/collaboration-color-service.ts b/packages/collaboration/src/browser/collaboration-color-service.ts new file mode 100644 index 0000000000000..adfac8e819118 --- /dev/null +++ b/packages/collaboration/src/browser/collaboration-color-service.ts @@ -0,0 +1,77 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; + +export interface CollaborationColor { + r: number; + g: number; + b: number; +} + +export namespace CollaborationColor { + export function fromString(code: string): CollaborationColor { + if (code.startsWith('#')) { + code = code.substring(1); + } + const r = parseInt(code.substring(0, 2), 16); + const g = parseInt(code.substring(2, 4), 16); + const b = parseInt(code.substring(4, 6), 16); + return { r, g, b }; + } + + export const Gold = fromString('#FFD700'); + export const Tomato = fromString('#FF6347'); + export const Aquamarine = fromString('#7FFFD4'); + export const Beige = fromString('#F5F5DC'); + export const Coral = fromString('#FF7F50'); + export const DarkOrange = fromString('#FF8C00'); + export const VioletRed = fromString('#C71585'); + export const DodgerBlue = fromString('#1E90FF'); + export const Chocolate = fromString('#D2691E'); + export const LightGreen = fromString('#90EE90'); + export const MediumOrchid = fromString('#BA55D3'); + export const Orange = fromString('#FFA500'); +} + +@injectable() +export class CollaborationColorService { + + light = 'white'; + dark = 'black'; + + getColors(): CollaborationColor[] { + return [ + CollaborationColor.Gold, + CollaborationColor.Aquamarine, + CollaborationColor.Tomato, + CollaborationColor.MediumOrchid, + CollaborationColor.LightGreen, + CollaborationColor.Orange, + CollaborationColor.Beige, + CollaborationColor.Chocolate, + CollaborationColor.VioletRed, + CollaborationColor.Coral, + CollaborationColor.DodgerBlue, + CollaborationColor.DarkOrange + ]; + } + + requiresDarkFont(color: CollaborationColor): boolean { + // From https://stackoverflow.com/a/3943023 + return ((color.r * 0.299) + (color.g * 0.587) + (color.b * 0.114)) > 186; + } +} diff --git a/packages/collaboration/src/browser/collaboration-file-system-provider.ts b/packages/collaboration/src/browser/collaboration-file-system-provider.ts new file mode 100644 index 0000000000000..87c9f556b0d57 --- /dev/null +++ b/packages/collaboration/src/browser/collaboration-file-system-provider.ts @@ -0,0 +1,119 @@ +// ***************************************************************************** +// 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 * as Y from 'yjs'; +import { Disposable, Emitter, Event, URI } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { + FileChange, FileDeleteOptions, + FileOverwriteOptions, FileSystemProviderCapabilities, FileType, Stat, WatchOptions, FileSystemProviderWithFileReadWriteCapability, FileWriteOptions +} from '@theia/filesystem/lib/common/files'; +import { ProtocolBroadcastConnection, Workspace, Peer } from 'open-collaboration-protocol'; + +export namespace CollaborationURI { + + export const scheme = 'collaboration'; + + export function create(workspace: Workspace, path?: string): URI { + return new URI(`${scheme}:///${workspace.name}${path ? '/' + path : ''}`); + } +} + +@injectable() +export class CollaborationFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability { + + capabilities = FileSystemProviderCapabilities.FileReadWrite; + + protected _readonly: boolean; + + get readonly(): boolean { + return this._readonly; + } + + set readonly(value: boolean) { + if (this._readonly !== value) { + this._readonly = value; + if (value) { + this.capabilities |= FileSystemProviderCapabilities.Readonly; + } else { + this.capabilities &= ~FileSystemProviderCapabilities.Readonly; + } + this.onDidChangeCapabilitiesEmitter.fire(); + } + } + + constructor(readonly connection: ProtocolBroadcastConnection, readonly host: Peer, readonly yjs: Y.Doc) { + } + + protected encoder = new TextEncoder(); + protected decoder = new TextDecoder(); + protected onDidChangeCapabilitiesEmitter = new Emitter(); + protected onDidChangeFileEmitter = new Emitter(); + protected onFileWatchErrorEmitter = new Emitter(); + + get onDidChangeCapabilities(): Event { + return this.onDidChangeCapabilitiesEmitter.event; + } + get onDidChangeFile(): Event { + return this.onDidChangeFileEmitter.event; + } + get onFileWatchError(): Event { + return this.onFileWatchErrorEmitter.event; + } + async readFile(resource: URI): Promise { + const path = this.getHostPath(resource); + if (this.yjs.share.has(path)) { + const stringValue = this.yjs.getText(path); + return this.encoder.encode(stringValue.toString()); + } else { + const data = await this.connection.fs.readFile(this.host.id, path); + return data.content; + } + } + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + const path = this.getHostPath(resource); + await this.connection.fs.writeFile(this.host.id, path, { content }); + } + watch(resource: URI, opts: WatchOptions): Disposable { + return Disposable.NULL; + } + stat(resource: URI): Promise { + return this.connection.fs.stat(this.host.id, this.getHostPath(resource)); + } + mkdir(resource: URI): Promise { + return this.connection.fs.mkdir(this.host.id, this.getHostPath(resource)); + } + async readdir(resource: URI): Promise<[string, FileType][]> { + const record = await this.connection.fs.readdir(this.host.id, this.getHostPath(resource)); + return Object.entries(record); + } + delete(resource: URI, opts: FileDeleteOptions): Promise { + return this.connection.fs.delete(this.host.id, this.getHostPath(resource)); + } + rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + return this.connection.fs.rename(this.host.id, this.getHostPath(from), this.getHostPath(to)); + } + + protected getHostPath(uri: URI): string { + const path = uri.path.toString().substring(1).split('/'); + return path.slice(1).join('/'); + } + + triggerEvent(changes: FileChange[]): void { + this.onDidChangeFileEmitter.fire(changes); + } + +} diff --git a/packages/collaboration/src/browser/collaboration-frontend-contribution.ts b/packages/collaboration/src/browser/collaboration-frontend-contribution.ts new file mode 100644 index 0000000000000..3d8784b84a9b3 --- /dev/null +++ b/packages/collaboration/src/browser/collaboration-frontend-contribution.ts @@ -0,0 +1,327 @@ +// ***************************************************************************** +// 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 '../../src/browser/style/index.css'; + +import { + CancellationToken, CancellationTokenSource, Command, CommandContribution, CommandRegistry, MessageService, nls, Progress, QuickInputService, QuickPickItem +} from '@theia/core'; +import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify'; +import { ConnectionProvider, SocketIoTransportProvider } from 'open-collaboration-protocol'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { CollaborationInstance, CollaborationInstanceFactory } from './collaboration-instance'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { CollaborationWorkspaceService } from './collaboration-workspace-service'; +import { StatusBar, StatusBarAlignment, StatusBarEntry } from '@theia/core/lib/browser/status-bar'; +import { codiconArray } from '@theia/core/lib/browser/widgets/widget'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; + +export const COLLABORATION_CATEGORY = 'Collaboration'; + +export namespace CollaborationCommands { + export const CREATE_ROOM: Command = { + id: 'collaboration.create-room' + }; + export const JOIN_ROOM: Command = { + id: 'collaboration.join-room' + }; +} + +export const COLLABORATION_STATUS_BAR_ID = 'statusBar.collaboration'; + +export const COLLABORATION_AUTH_TOKEN = 'THEIA_COLLAB_AUTH_TOKEN'; +export const COLLABORATION_SERVER_URL = 'COLLABORATION_SERVER_URL'; +export const DEFAULT_COLLABORATION_SERVER_URL = 'https://api.open-collab.tools/'; + +@injectable() +export class CollaborationFrontendContribution implements CommandContribution { + + protected readonly connectionProvider = new Deferred(); + + @inject(WindowService) + protected readonly windowService: WindowService; + + @inject(QuickInputService) @optional() + protected readonly quickInputService?: QuickInputService; + + @inject(EnvVariablesServer) + protected readonly envVariables: EnvVariablesServer; + + @inject(CollaborationWorkspaceService) + protected readonly workspaceService: CollaborationWorkspaceService; + + @inject(MessageService) + protected readonly messageService: MessageService; + + @inject(CommandRegistry) + protected readonly commands: CommandRegistry; + + @inject(StatusBar) + protected readonly statusBar: StatusBar; + + @inject(CollaborationInstanceFactory) + protected readonly collaborationInstanceFactory: CollaborationInstanceFactory; + + protected currentInstance?: CollaborationInstance; + + @postConstruct() + protected init(): void { + this.setStatusBarEntryDefault(); + this.getCollaborationServerUrl().then(serverUrl => { + const authHandler = new ConnectionProvider({ + url: serverUrl, + client: FrontendApplicationConfigProvider.get().applicationName, + fetch: window.fetch.bind(window), + opener: url => this.windowService.openNewWindow(url, { external: true }), + transports: [SocketIoTransportProvider], + userToken: localStorage.getItem(COLLABORATION_AUTH_TOKEN) ?? undefined + }); + this.connectionProvider.resolve(authHandler); + }, err => this.connectionProvider.reject(err)); + } + + protected async onStatusDefaultClick(): Promise { + const items: QuickPickItem[] = []; + if (this.workspaceService.opened) { + items.push({ + label: nls.localize('theia/collaboration/createRoom', 'Create New Collaboration Session'), + iconClasses: codiconArray('add'), + execute: () => this.commands.executeCommand(CollaborationCommands.CREATE_ROOM.id) + }); + } + items.push({ + label: nls.localize('theia/collaboration/joinRoom', 'Join Collaboration Session'), + iconClasses: codiconArray('vm-connect'), + execute: () => this.commands.executeCommand(CollaborationCommands.JOIN_ROOM.id) + }); + await this.quickInputService?.showQuickPick(items, { + placeholder: nls.localize('theia/collaboration/selectCollaboration', 'Select collaboration option') + }); + } + + protected async onStatusSharedClick(code: string): Promise { + const items: QuickPickItem[] = [{ + label: nls.localize('theia/collaboration/invite', 'Invite Others'), + detail: nls.localize('theia/collaboration/inviteDetail', 'Copy the invitation code for sharing it with others to join the session.'), + iconClasses: codiconArray('clippy'), + execute: () => this.displayCopyNotification(code) + }]; + if (this.currentInstance) { + // TODO: Implement readonly mode + // if (this.currentInstance.readonly) { + // items.push({ + // label: nls.localize('theia/collaboration/enableEditing', 'Enable Workspace Editing'), + // detail: nls.localize('theia/collaboration/enableEditingDetail', 'Allow collaborators to modify content in your workspace.'), + // iconClasses: codiconArray('unlock'), + // execute: () => { + // if (this.currentInstance) { + // this.currentInstance.readonly = false; + // } + // } + // }); + // } else { + // items.push({ + // label: nls.localize('theia/collaboration/disableEditing', 'Disable Workspace Editing'), + // detail: nls.localize('theia/collaboration/disableEditingDetail', 'Restrict others from making changes to your workspace.'), + // iconClasses: codiconArray('lock'), + // execute: () => { + // if (this.currentInstance) { + // this.currentInstance.readonly = true; + // } + // } + // }); + // } + } + items.push({ + label: nls.localize('theia/collaboration/end', 'End Collaboration Session'), + detail: nls.localize('theia/collaboration/endDetail', 'Terminate the session, cease content sharing, and revoke access for others.'), + iconClasses: codiconArray('circle-slash'), + execute: () => this.currentInstance?.dispose() + }); + await this.quickInputService?.showQuickPick(items, { + placeholder: nls.localize('theia/collaboration/whatToDo', 'What would you like to do with other collaborators?') + }); + } + + protected async onStatusConnectedClick(code: string): Promise { + const items: QuickPickItem[] = [{ + label: nls.localize('theia/collaboration/invite', 'Invite Others'), + detail: nls.localize('theia/collaboration/inviteDetail', 'Copy the invitation code for sharing it with others to join the session.'), + iconClasses: codiconArray('clippy'), + execute: () => this.displayCopyNotification(code) + }]; + items.push({ + label: nls.localize('theia/collaboration/leave', 'Leave Collaboration Session'), + detail: nls.localize('theia/collaboration/leaveDetail', 'Disconnect from the current collaboration session and close the workspace.'), + iconClasses: codiconArray('circle-slash'), + execute: () => this.currentInstance?.dispose() + }); + await this.quickInputService?.showQuickPick(items, { + placeholder: nls.localize('theia/collaboration/whatToDo', 'What would you like to do with other collaborators?') + }); + } + + protected async setStatusBarEntryDefault(): Promise { + await this.setStatusBarEntry({ + text: '$(codicon-live-share) ' + nls.localize('theia/collaboration/collaborate', 'Collaborate'), + tooltip: nls.localize('theia/collaboration/startSession', 'Start or join collaboration session'), + onclick: () => this.onStatusDefaultClick() + }); + } + + protected async setStatusBarEntryShared(code: string): Promise { + await this.setStatusBarEntry({ + text: '$(codicon-broadcast) ' + nls.localizeByDefault('Shared'), + tooltip: nls.localize('theia/collaboration/sharedSession', 'Shared a collaboration session'), + onclick: () => this.onStatusSharedClick(code) + }); + } + + protected async setStatusBarEntryConnected(code: string): Promise { + await this.setStatusBarEntry({ + text: '$(codicon-broadcast) ' + nls.localize('theia/collaboration/connected', 'Connected'), + tooltip: nls.localize('theia/collaboration/connectedSession', 'Connected to a collaboration session'), + onclick: () => this.onStatusConnectedClick(code) + }); + } + + protected async setStatusBarEntry(entry: Omit): Promise { + await this.statusBar.setElement(COLLABORATION_STATUS_BAR_ID, { + ...entry, + alignment: StatusBarAlignment.LEFT, + priority: 5 + }); + } + + protected async getCollaborationServerUrl(): Promise { + const serverUrlVariable = await this.envVariables.getValue(COLLABORATION_SERVER_URL); + return serverUrlVariable?.value || DEFAULT_COLLABORATION_SERVER_URL; + } + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(CollaborationCommands.CREATE_ROOM, { + execute: async () => { + const cancelTokenSource = new CancellationTokenSource(); + const progress = await this.messageService.showProgress({ + text: nls.localize('theia/collaboration/creatingRoom', 'Creating Session'), + options: { + cancelable: true + } + }, () => cancelTokenSource.cancel()); + try { + const authHandler = await this.connectionProvider.promise; + const roomClaim = await authHandler.createRoom({ + reporter: info => progress.report({ message: info.message }), + abortSignal: this.toAbortSignal(cancelTokenSource.token) + }); + if (roomClaim.loginToken) { + localStorage.setItem(COLLABORATION_AUTH_TOKEN, roomClaim.loginToken); + } + this.currentInstance?.dispose(); + const connection = await authHandler.connect(roomClaim.roomToken); + this.currentInstance = this.collaborationInstanceFactory({ + role: 'host', + connection + }); + this.currentInstance.onDidClose(() => { + this.setStatusBarEntryDefault(); + }); + const roomCode = roomClaim.roomId; + this.setStatusBarEntryShared(roomCode); + this.displayCopyNotification(roomCode, true); + } catch (err) { + await this.messageService.error(nls.localize('theia/collaboration/failedCreate', 'Failed to create room: {0}', err.message)); + } finally { + progress.cancel(); + } + } + }); + commands.registerCommand(CollaborationCommands.JOIN_ROOM, { + execute: async () => { + let joinRoomProgress: Progress | undefined; + const cancelTokenSource = new CancellationTokenSource(); + try { + const authHandler = await this.connectionProvider.promise; + const id = await this.quickInputService?.input({ + placeHolder: nls.localize('theia/collaboration/enterCode', 'Enter collaboration session code') + }); + if (!id) { + return; + } + joinRoomProgress = await this.messageService.showProgress({ + text: nls.localize('theia/collaboration/joiningRoom', 'Joining Session'), + options: { + cancelable: true + } + }, () => cancelTokenSource.cancel()); + const roomClaim = await authHandler.joinRoom({ + roomId: id, + reporter: info => joinRoomProgress?.report({ message: info.message }), + abortSignal: this.toAbortSignal(cancelTokenSource.token) + }); + joinRoomProgress.cancel(); + if (roomClaim.loginToken) { + localStorage.setItem(COLLABORATION_AUTH_TOKEN, roomClaim.loginToken); + } + this.currentInstance?.dispose(); + const connection = await authHandler.connect(roomClaim.roomToken, roomClaim.host); + this.currentInstance = this.collaborationInstanceFactory({ + role: 'guest', + connection + }); + this.currentInstance.onDidClose(() => { + this.setStatusBarEntryDefault(); + }); + this.setStatusBarEntryConnected(roomClaim.roomId); + } catch (err) { + joinRoomProgress?.cancel(); + await this.messageService.error(nls.localize('theia/collaboration/failedJoin', 'Failed to join room: {0}', err.message)); + } + } + }); + } + + protected toAbortSignal(...tokens: CancellationToken[]): AbortSignal { + const controller = new AbortController(); + tokens.forEach(token => token.onCancellationRequested(() => controller.abort())); + return controller.signal; + } + + protected async displayCopyNotification(code: string, firstTime = false): Promise { + navigator.clipboard.writeText(code); + const notification = nls.localize('theia/collaboration/copiedInvitation', 'Invitation code copied to clipboard.'); + if (firstTime) { + // const makeReadonly = nls.localize('theia/collaboration/makeReadonly', 'Make readonly'); + const copyAgain = nls.localize('theia/collaboration/copyAgain', 'Copy Again'); + const copyResult = await this.messageService.info( + notification, + // makeReadonly, + copyAgain + ); + // if (copyResult === makeReadonly && this.currentInstance) { + // this.currentInstance.readonly = true; + // } + if (copyResult === copyAgain) { + navigator.clipboard.writeText(code); + } + } else { + await this.messageService.info( + notification + ); + } + } +} diff --git a/packages/collaboration/src/browser/collaboration-frontend-module.ts b/packages/collaboration/src/browser/collaboration-frontend-module.ts new file mode 100644 index 0000000000000..f0b9080e3113c --- /dev/null +++ b/packages/collaboration/src/browser/collaboration-frontend-module.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// 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 } from '@theia/core'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { CollaborationColorService } from './collaboration-color-service'; +import { CollaborationFrontendContribution } from './collaboration-frontend-contribution'; +import { CollaborationInstance, CollaborationInstanceFactory, CollaborationInstanceOptions, createCollaborationInstanceContainer } from './collaboration-instance'; +import { CollaborationUtils } from './collaboration-utils'; +import { CollaborationWorkspaceService } from './collaboration-workspace-service'; + +export default new ContainerModule((bind, _, __, rebind) => { + bind(CollaborationWorkspaceService).toSelf().inSingletonScope(); + rebind(WorkspaceService).toService(CollaborationWorkspaceService); + bind(CollaborationUtils).toSelf().inSingletonScope(); + bind(CollaborationFrontendContribution).toSelf().inSingletonScope(); + bind(CommandContribution).toService(CollaborationFrontendContribution); + bind(CollaborationInstanceFactory).toFactory(context => (options: CollaborationInstanceOptions) => { + const container = createCollaborationInstanceContainer(context.container, options); + return container.get(CollaborationInstance); + }); + bind(CollaborationColorService).toSelf().inSingletonScope(); +}); diff --git a/packages/collaboration/src/browser/collaboration-instance.ts b/packages/collaboration/src/browser/collaboration-instance.ts new file mode 100644 index 0000000000000..00c89e1908a77 --- /dev/null +++ b/packages/collaboration/src/browser/collaboration-instance.ts @@ -0,0 +1,819 @@ +// ***************************************************************************** +// 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 * as types from 'open-collaboration-protocol'; +import * as Y from 'yjs'; +import * as awarenessProtocol from 'y-protocols/awareness'; + +import { Disposable, DisposableCollection, Emitter, Event, MessageService, URI, nls } from '@theia/core'; +import { Container, inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; +import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; +import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; +import { CollaborationWorkspaceService } from './collaboration-workspace-service'; +import { Range as MonacoRange } from '@theia/monaco-editor-core'; +import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { EditorDecoration, EditorWidget, Selection, TextEditorDocument, TrackedRangeStickiness } from '@theia/editor/lib/browser'; +import { DecorationStyle, OpenerService, SaveReason } from '@theia/core/lib/browser'; +import { CollaborationFileSystemProvider, CollaborationURI } from './collaboration-file-system-provider'; +import { Range } from '@theia/core/shared/vscode-languageserver-protocol'; +import { CollaborationColorService } from './collaboration-color-service'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; +import { FileChange, FileChangeType, FileOperation } from '@theia/filesystem/lib/common/files'; +import { OpenCollaborationYjsProvider } from 'open-collaboration-yjs'; +import { createMutex } from 'lib0/mutex'; +import { CollaborationUtils } from './collaboration-utils'; +import debounce = require('@theia/core/shared/lodash.debounce'); + +export const CollaborationInstanceFactory = Symbol('CollaborationInstanceFactory'); +export type CollaborationInstanceFactory = (connection: CollaborationInstanceOptions) => CollaborationInstance; + +export const CollaborationInstanceOptions = Symbol('CollaborationInstanceOptions'); +export interface CollaborationInstanceOptions { + role: 'host' | 'guest'; + connection: types.ProtocolBroadcastConnection; +} + +export function createCollaborationInstanceContainer(parent: interfaces.Container, options: CollaborationInstanceOptions): Container { + const child = new Container(); + child.parent = parent; + child.bind(CollaborationInstance).toSelf().inTransientScope(); + child.bind(CollaborationInstanceOptions).toConstantValue(options); + return child; +} + +export interface DisposablePeer extends Disposable { + peer: types.Peer; +} + +export const COLLABORATION_SELECTION = 'theia-collaboration-selection'; +export const COLLABORATION_SELECTION_MARKER = 'theia-collaboration-selection-marker'; +export const COLLABORATION_SELECTION_INVERTED = 'theia-collaboration-selection-inverted'; + +@injectable() +export class CollaborationInstance implements Disposable { + + @inject(MessageService) + protected readonly messageService: MessageService; + + @inject(CollaborationWorkspaceService) + protected readonly workspaceService: CollaborationWorkspaceService; + + @inject(FileService) + protected readonly fileService: FileService; + + @inject(MonacoTextModelService) + protected readonly monacoModelService: MonacoTextModelService; + + @inject(EditorManager) + protected readonly editorManager: EditorManager; + + @inject(OpenerService) + protected readonly openerService: OpenerService; + + @inject(ApplicationShell) + protected readonly shell: ApplicationShell; + + @inject(CollaborationInstanceOptions) + protected readonly options: CollaborationInstanceOptions; + + @inject(CollaborationColorService) + protected readonly collaborationColorService: CollaborationColorService; + + @inject(CollaborationUtils) + protected readonly utils: CollaborationUtils; + + protected identity = new Deferred(); + protected peers = new Map(); + protected yjs = new Y.Doc(); + protected yjsAwareness = new awarenessProtocol.Awareness(this.yjs); + protected yjsProvider: OpenCollaborationYjsProvider; + protected colorIndex = 0; + protected editorDecorations = new Map(); + protected fileSystem?: CollaborationFileSystemProvider; + protected permissions: types.Permissions = { + readonly: false + }; + + protected onDidCloseEmitter = new Emitter(); + + get onDidClose(): Event { + return this.onDidCloseEmitter.event; + } + + protected toDispose = new DisposableCollection(); + protected _readonly = false; + + get readonly(): boolean { + return this._readonly; + } + + set readonly(value: boolean) { + if (value !== this.readonly) { + if (this.options.role === 'guest' && this.fileSystem) { + this.fileSystem.readonly = value; + } else if (this.options.role === 'host') { + this.options.connection.room.updatePermissions({ + ...(this.permissions ?? {}), + readonly: value + }); + } + if (this.permissions) { + this.permissions.readonly = value; + } + this._readonly = value; + } + } + + get isHost(): boolean { + return this.options.role === 'host'; + } + + get host(): types.Peer { + return Array.from(this.peers.values()).find(e => e.peer.host)!.peer; + } + + @postConstruct() + protected init(): void { + const connection = this.options.connection; + connection.onDisconnect(() => this.dispose()); + connection.onConnectionError(message => { + this.messageService.error(message); + this.dispose(); + }); + this.yjsProvider = new OpenCollaborationYjsProvider(connection, this.yjs, this.yjsAwareness); + this.yjsProvider.connect(); + this.toDispose.push(Disposable.create(() => this.yjs.destroy())); + this.toDispose.push(this.yjsProvider); + this.toDispose.push(connection); + this.toDispose.push(this.onDidCloseEmitter); + + this.registerProtocolEvents(connection); + this.registerEditorEvents(connection); + this.registerFileSystemEvents(connection); + + if (this.isHost) { + this.registerFileSystemChanges(); + } + } + + protected registerProtocolEvents(connection: types.ProtocolBroadcastConnection): void { + connection.peer.onJoinRequest(async (_, user) => { + const allow = nls.localizeByDefault('Allow'); + const deny = nls.localizeByDefault('Deny'); + const result = await this.messageService.info( + nls.localize('theia/collaboration/userWantsToJoin', "User '{0}' wants to join the collaboration room", user.email ? `${user.name} (${user.email})` : user.name), + allow, + deny + ); + if (result === allow) { + const roots = await this.workspaceService.roots; + return { + workspace: { + name: this.workspaceService.workspace?.name ?? nls.localize('theia/collaboration/collaboration', 'Collaboration'), + folders: roots.map(e => e.name) + } + }; + } else { + return undefined; + } + }); + connection.room.onJoin(async (_, peer) => { + this.addPeer(peer); + if (this.isHost) { + const roots = await this.workspaceService.roots; + const data: types.InitData = { + protocol: types.VERSION, + host: await this.identity.promise, + guests: Array.from(this.peers.values()).map(e => e.peer), + capabilities: {}, + permissions: this.permissions, + workspace: { + name: this.workspaceService.workspace?.name ?? nls.localize('theia/collaboration/collaboration', 'Collaboration'), + folders: roots.map(e => e.name) + } + }; + connection.peer.init(peer.id, data); + } + }); + connection.room.onLeave((_, peer) => { + this.peers.get(peer.id)?.dispose(); + }); + connection.room.onClose(() => { + this.dispose(); + }); + connection.room.onPermissions((_, permissions) => { + if (this.fileSystem) { + this.fileSystem.readonly = permissions.readonly; + } + }); + connection.peer.onInfo((_, peer) => { + this.yjsAwareness.setLocalStateField('peer', peer.id); + this.identity.resolve(peer); + }); + connection.peer.onInit(async (_, data) => { + await this.initialize(data); + }); + } + + protected registerEditorEvents(connection: types.ProtocolBroadcastConnection): void { + for (const model of this.monacoModelService.models) { + if (this.isSharedResource(new URI(model.uri))) { + this.registerModelUpdate(model); + } + } + this.toDispose.push(this.monacoModelService.onDidCreate(newModel => { + if (this.isSharedResource(new URI(newModel.uri))) { + this.registerModelUpdate(newModel); + } + })); + this.toDispose.push(this.editorManager.onCreated(widget => { + if (this.isSharedResource(widget.getResourceUri())) { + this.registerPresenceUpdate(widget); + } + })); + this.getOpenEditors().forEach(widget => { + if (this.isSharedResource(widget.getResourceUri())) { + this.registerPresenceUpdate(widget); + } + }); + this.shell.onDidChangeActiveWidget(e => { + if (e.newValue instanceof EditorWidget) { + this.updateEditorPresence(e.newValue); + } + }); + + this.yjsAwareness.on('change', () => { + this.rerenderPresence(); + }); + + connection.editor.onOpen(async (_, path) => { + const uri = this.utils.getResourceUri(path); + if (uri) { + await this.openUri(uri); + } else { + throw new Error('Could find file: ' + path); + } + return undefined; + }); + } + + protected isSharedResource(resource?: URI): boolean { + if (!resource) { + return false; + } + return this.isHost ? resource.scheme === 'file' : resource.scheme === CollaborationURI.scheme; + } + + protected registerFileSystemEvents(connection: types.ProtocolBroadcastConnection): void { + connection.fs.onReadFile(async (_, path) => { + const uri = this.utils.getResourceUri(path); + if (uri) { + const content = await this.fileService.readFile(uri); + return { + content: content.value.buffer + }; + } else { + throw new Error('Could find file: ' + path); + } + }); + connection.fs.onReaddir(async (_, path) => { + const uri = this.utils.getResourceUri(path); + if (uri) { + const resolved = await this.fileService.resolve(uri); + if (resolved.children) { + const dir: Record = {}; + for (const child of resolved.children) { + dir[child.name] = child.isDirectory ? types.FileType.Directory : types.FileType.File; + } + return dir; + } else { + return {}; + } + } else { + throw new Error('Could find directory: ' + path); + } + }); + connection.fs.onStat(async (_, path) => { + const uri = this.utils.getResourceUri(path); + if (uri) { + const content = await this.fileService.resolve(uri, { + resolveMetadata: true + }); + return { + type: content.isDirectory ? types.FileType.Directory : types.FileType.File, + ctime: content.ctime, + mtime: content.mtime, + size: content.size, + permissions: content.isReadonly ? types.FilePermission.Readonly : undefined + }; + } else { + throw new Error('Could find file: ' + path); + } + }); + connection.fs.onWriteFile(async (_, path, data) => { + const uri = this.utils.getResourceUri(path); + if (uri) { + const model = this.getModel(uri); + if (model) { + const content = new TextDecoder().decode(data.content); + if (content !== model.getText()) { + model.textEditorModel.setValue(content); + } + await model.save({ saveReason: SaveReason.Manual }); + } else { + await this.fileService.createFile(uri, BinaryBuffer.wrap(data.content)); + } + } else { + throw new Error('Could find file: ' + path); + } + }); + connection.fs.onMkdir(async (_, path) => { + const uri = this.utils.getResourceUri(path); + if (uri) { + await this.fileService.createFolder(uri); + } else { + throw new Error('Could find path: ' + path); + } + }); + connection.fs.onDelete(async (_, path) => { + const uri = this.utils.getResourceUri(path); + if (uri) { + await this.fileService.delete(uri); + } else { + throw new Error('Could find entry: ' + path); + } + }); + connection.fs.onRename(async (_, from, to) => { + const fromUri = this.utils.getResourceUri(from); + const toUri = this.utils.getResourceUri(to); + if (fromUri && toUri) { + await this.fileService.move(fromUri, toUri); + } else { + throw new Error('Could find entries: ' + from + ' -> ' + to); + } + }); + connection.fs.onChange(async (_, event) => { + // Only guests need to handle file system changes + if (!this.isHost && this.fileSystem) { + const changes: FileChange[] = []; + for (const change of event.changes) { + const uri = this.utils.getResourceUri(change.path); + if (uri) { + changes.push({ + type: change.type === types.FileChangeEventType.Create + ? FileChangeType.ADDED + : change.type === types.FileChangeEventType.Update + ? FileChangeType.UPDATED + : FileChangeType.DELETED, + resource: uri + }); + } + } + this.fileSystem.triggerEvent(changes); + } + }); + } + + protected rerenderPresence(...widgets: EditorWidget[]): void { + const decorations = new Map(); + const states = this.yjsAwareness.getStates() as Map; + for (const [clientID, state] of states.entries()) { + if (clientID === this.yjs.clientID) { + // Ignore own awareness state + continue; + } + const peer = state.peer; + if (!state.selection || !this.peers.has(peer)) { + continue; + } + if (!types.ClientTextSelection.is(state.selection)) { + continue; + } + const { path, textSelections } = state.selection; + const selection = textSelections[0]; + if (!selection) { + continue; + } + const uri = this.utils.getResourceUri(path); + if (uri) { + const model = this.getModel(uri); + if (model) { + let existing = decorations.get(path); + if (!existing) { + existing = []; + decorations.set(path, existing); + } + const forward = selection.direction === types.SelectionDirection.LeftToRight; + let startIndex = Y.createAbsolutePositionFromRelativePosition(selection.start, this.yjs); + let endIndex = Y.createAbsolutePositionFromRelativePosition(selection.end, this.yjs); + if (startIndex && endIndex) { + if (startIndex.index > endIndex.index) { + [startIndex, endIndex] = [endIndex, startIndex]; + } + const start = model.positionAt(startIndex.index); + const end = model.positionAt(endIndex.index); + const inverted = (forward && end.line === 0) || (!forward && start.line === 0); + const range = { + start, + end + }; + const contentClassNames: string[] = [COLLABORATION_SELECTION_MARKER, `${COLLABORATION_SELECTION_MARKER}-${peer}`]; + if (inverted) { + contentClassNames.push(COLLABORATION_SELECTION_INVERTED); + } + const item: EditorDecoration = { + range, + options: { + className: `${COLLABORATION_SELECTION} ${COLLABORATION_SELECTION}-${peer}`, + beforeContentClassName: !forward ? contentClassNames.join(' ') : undefined, + afterContentClassName: forward ? contentClassNames.join(' ') : undefined, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + } + }; + existing.push(item); + } + } + } + } + this.rerenderPresenceDecorations(decorations, ...widgets); + } + + protected rerenderPresenceDecorations(decorations: Map, ...widgets: EditorWidget[]): void { + for (const editor of new Set(this.getOpenEditors().concat(widgets))) { + const uri = editor.getResourceUri(); + const path = this.utils.getProtocolPath(uri); + if (path) { + const old = this.editorDecorations.get(editor) ?? []; + this.editorDecorations.set(editor, editor.editor.deltaDecorations({ + newDecorations: decorations.get(path) ?? [], + oldDecorations: old + })); + } + } + } + + protected registerFileSystemChanges(): void { + // Event listener for disk based events + this.fileService.onDidFilesChange(event => { + const changes: types.FileChange[] = []; + for (const change of event.changes) { + const path = this.utils.getProtocolPath(change.resource); + if (path) { + let type: types.FileChangeEventType | undefined; + if (change.type === FileChangeType.ADDED) { + type = types.FileChangeEventType.Create; + } else if (change.type === FileChangeType.DELETED) { + type = types.FileChangeEventType.Delete; + } + // Updates to files on disk are not sent + if (type !== undefined) { + changes.push({ + path, + type + }); + } + } + } + if (changes.length) { + this.options.connection.fs.change({ changes }); + } + }); + // Event listener for user based events + this.fileService.onDidRunOperation(operation => { + const path = this.utils.getProtocolPath(operation.resource); + if (!path) { + return; + } + let type = types.FileChangeEventType.Update; + if (operation.isOperation(FileOperation.CREATE) || operation.isOperation(FileOperation.COPY)) { + type = types.FileChangeEventType.Create; + } else if (operation.isOperation(FileOperation.DELETE)) { + type = types.FileChangeEventType.Delete; + } + this.options.connection.fs.change({ + changes: [{ + path, + type + }] + }); + }); + } + + protected async registerPresenceUpdate(widget: EditorWidget): Promise { + const uri = widget.getResourceUri(); + const path = this.utils.getProtocolPath(uri); + if (path) { + if (!this.isHost) { + this.options.connection.editor.open(this.host.id, path); + } + let currentSelection = widget.editor.selection; + // // Update presence information when the selection changes + const selectionChange = widget.editor.onSelectionChanged(selection => { + if (!this.rangeEqual(currentSelection, selection)) { + this.updateEditorPresence(widget); + currentSelection = selection; + } + }); + const widgetDispose = widget.onDidDispose(() => { + widgetDispose.dispose(); + selectionChange.dispose(); + // Remove presence information when the editor closes + const state = this.yjsAwareness.getLocalState(); + if (state?.currentSelection?.path === path) { + delete state.currentSelection; + } + this.yjsAwareness.setLocalState(state); + }); + this.toDispose.push(selectionChange); + this.toDispose.push(widgetDispose); + this.rerenderPresence(widget); + } + } + + protected updateEditorPresence(widget: EditorWidget): void { + const uri = widget.getResourceUri(); + const path = this.utils.getProtocolPath(uri); + if (path) { + const ytext = this.yjs.getText(path); + const selection = widget.editor.selection; + let start = widget.editor.document.offsetAt(selection.start); + let end = widget.editor.document.offsetAt(selection.end); + if (start > end) { + [start, end] = [end, start]; + } + const direction = selection.direction === 'ltr' + ? types.SelectionDirection.LeftToRight + : types.SelectionDirection.RightToLeft; + const editorSelection: types.RelativeTextSelection = { + start: Y.createRelativePositionFromTypeIndex(ytext, start), + end: Y.createRelativePositionFromTypeIndex(ytext, end), + direction + }; + const textSelection: types.ClientTextSelection = { + path, + textSelections: [editorSelection] + }; + this.setSharedSelection(textSelection); + } + } + + protected setSharedSelection(selection?: types.ClientSelection): void { + this.yjsAwareness.setLocalStateField('selection', selection); + } + + protected rangeEqual(a: Range, b: Range): boolean { + return a.start.line === b.start.line + && a.start.character === b.start.character + && a.end.line === b.end.line + && a.end.character === b.end.character; + } + + async initialize(data: types.InitData): Promise { + this.permissions = data.permissions; + this.readonly = data.permissions.readonly; + for (const peer of [...data.guests, data.host]) { + this.addPeer(peer); + } + this.fileSystem = new CollaborationFileSystemProvider(this.options.connection, data.host, this.yjs); + this.fileSystem.readonly = this.readonly; + this.toDispose.push(this.fileService.registerProvider(CollaborationURI.scheme, this.fileSystem)); + const workspaceDisposable = await this.workspaceService.setHostWorkspace(data.workspace, this.options.connection); + this.toDispose.push(workspaceDisposable); + } + + protected addPeer(peer: types.Peer): void { + const collection = new DisposableCollection(); + collection.push(this.createPeerStyleSheet(peer)); + collection.push(Disposable.create(() => this.peers.delete(peer.id))); + const disposablePeer = { + peer, + dispose: () => collection.dispose() + }; + this.peers.set(peer.id, disposablePeer); + } + + protected createPeerStyleSheet(peer: types.Peer): Disposable { + const style = DecorationStyle.createStyleElement(`${peer.id}-collaboration-selection`); + const colors = this.collaborationColorService.getColors(); + const sheet = style.sheet!; + const color = colors[this.colorIndex++ % colors.length]; + const colorString = `rgb(${color.r}, ${color.g}, ${color.b})`; + sheet.insertRule(` + .${COLLABORATION_SELECTION}-${peer.id} { + opacity: 0.2; + background: ${colorString}; + } + `); + sheet.insertRule(` + .${COLLABORATION_SELECTION_MARKER}-${peer.id} { + background: ${colorString}; + border-color: ${colorString}; + }` + ); + sheet.insertRule(` + .${COLLABORATION_SELECTION_MARKER}-${peer.id}::after { + content: "${peer.name}"; + background: ${colorString}; + color: ${this.collaborationColorService.requiresDarkFont(color) + ? this.collaborationColorService.dark + : this.collaborationColorService.light}; + z-index: ${(100 + this.colorIndex).toFixed()} + }` + ); + return Disposable.create(() => style.remove()); + } + + protected getOpenEditors(uri?: URI): EditorWidget[] { + const widgets = this.shell.widgets; + let editors = widgets.filter(e => e instanceof EditorWidget) as EditorWidget[]; + if (uri) { + const uriString = uri.toString(); + editors = editors.filter(e => e.getResourceUri()?.toString() === uriString); + } + return editors; + } + + protected createSelectionFromRelative(selection: types.RelativeTextSelection, model: MonacoEditorModel): Selection | undefined { + const start = Y.createAbsolutePositionFromRelativePosition(selection.start, this.yjs); + const end = Y.createAbsolutePositionFromRelativePosition(selection.end, this.yjs); + if (start && end) { + return { + start: model.positionAt(start.index), + end: model.positionAt(end.index), + direction: selection.direction === types.SelectionDirection.LeftToRight ? 'ltr' : 'rtl' + }; + } + return undefined; + } + + protected createRelativeSelection(selection: Selection, model: TextEditorDocument, ytext: Y.Text): types.RelativeTextSelection { + const start = Y.createRelativePositionFromTypeIndex(ytext, model.offsetAt(selection.start)); + const end = Y.createRelativePositionFromTypeIndex(ytext, model.offsetAt(selection.end)); + return { + start, + end, + direction: selection.direction === 'ltr' + ? types.SelectionDirection.LeftToRight + : types.SelectionDirection.RightToLeft + }; + } + + protected readonly yjsMutex = createMutex(); + + protected registerModelUpdate(model: MonacoEditorModel): void { + let updating = false; + const modelPath = this.utils.getProtocolPath(new URI(model.uri)); + if (!modelPath) { + return; + } + const unknownModel = !this.yjs.share.has(modelPath); + const ytext = this.yjs.getText(modelPath); + const modelText = model.textEditorModel.getValue(); + if (this.isHost && unknownModel) { + // If we are hosting the room, set the initial content + // First off, reset the shared content to be empty + // This has the benefit of effectively clearing the memory of the shared content across all peers + // This is important because the shared content accumulates changes/memory usage over time + this.resetYjsText(ytext, modelText); + } else { + this.options.connection.editor.open(this.host.id, modelPath); + } + // The Ytext instance is our source of truth for the model content + // Sometimes (especially after a lot of sequential undo/redo operations) our model content can get out of sync + // This resyncs the model content with the Ytext content after a delay + const resyncDebounce = debounce(() => { + this.yjsMutex(() => { + const newContent = ytext.toString(); + if (model.textEditorModel.getValue() !== newContent) { + updating = true; + this.softReplaceModel(model, newContent); + updating = false; + } + }); + }, 200); + const disposable = new DisposableCollection(); + disposable.push(model.onDidChangeContent(e => { + if (updating) { + return; + } + this.yjsMutex(() => { + this.yjs.transact(() => { + for (const change of e.contentChanges) { + ytext.delete(change.rangeOffset, change.rangeLength); + ytext.insert(change.rangeOffset, change.text); + } + }); + resyncDebounce(); + }); + })); + + const observer = (textEvent: Y.YTextEvent) => { + if (textEvent.transaction.local || model.getText() === ytext.toString()) { + // Ignore local changes and changes that are already reflected in the model + return; + } + this.yjsMutex(() => { + updating = true; + try { + let index = 0; + const operations: { range: MonacoRange, text: string }[] = []; + textEvent.delta.forEach(delta => { + if (delta.retain !== undefined) { + index += delta.retain; + } else if (delta.insert !== undefined) { + const pos = model.textEditorModel.getPositionAt(index); + const range = new MonacoRange(pos.lineNumber, pos.column, pos.lineNumber, pos.column); + const insert = delta.insert as string; + operations.push({ range, text: insert }); + index += insert.length; + } else if (delta.delete !== undefined) { + const pos = model.textEditorModel.getPositionAt(index); + const endPos = model.textEditorModel.getPositionAt(index + delta.delete); + const range = new MonacoRange(pos.lineNumber, pos.column, endPos.lineNumber, endPos.column); + operations.push({ range, text: '' }); + } + }); + this.pushChangesToModel(model, operations); + } catch (err) { + console.error(err); + } + resyncDebounce(); + updating = false; + }); + }; + + ytext.observe(observer); + disposable.push(Disposable.create(() => ytext.unobserve(observer))); + model.onDispose(() => disposable.dispose()); + } + + protected resetYjsText(yjsText: Y.Text, text: string): void { + this.yjs.transact(() => { + yjsText.delete(0, yjsText.length); + yjsText.insert(0, text); + }); + } + + protected getModel(uri: URI): MonacoEditorModel | undefined { + const existing = this.monacoModelService.models.find(e => e.uri === uri.toString()); + if (existing) { + return existing; + } else { + return undefined; + } + } + + protected pushChangesToModel(model: MonacoEditorModel, changes: { range: MonacoRange, text: string, forceMoveMarkers?: boolean }[]): void { + const editor = MonacoEditor.findByDocument(this.editorManager, model)[0]; + const cursorState = editor?.getControl().getSelections() ?? []; + model.textEditorModel.pushStackElement(); + try { + model.textEditorModel.pushEditOperations(cursorState, changes, () => cursorState); + model.textEditorModel.pushStackElement(); + } catch (err) { + console.error(err); + } + } + + protected softReplaceModel(model: MonacoEditorModel, text: string): void { + this.pushChangesToModel(model, [{ + range: model.textEditorModel.getFullModelRange(), + text, + forceMoveMarkers: false + }]); + } + + protected async openUri(uri: URI): Promise { + const ref = await this.monacoModelService.createModelReference(uri); + if (ref.object) { + this.toDispose.push(ref); + } else { + ref.dispose(); + } + } + + dispose(): void { + for (const peer of this.peers.values()) { + peer.dispose(); + } + this.onDidCloseEmitter.fire(); + this.toDispose.dispose(); + } +} diff --git a/packages/collaboration/src/browser/collaboration-utils.ts b/packages/collaboration/src/browser/collaboration-utils.ts new file mode 100644 index 0000000000000..c1398033a4026 --- /dev/null +++ b/packages/collaboration/src/browser/collaboration-utils.ts @@ -0,0 +1,59 @@ +// ***************************************************************************** +// 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 { URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { CollaborationWorkspaceService } from './collaboration-workspace-service'; + +@injectable() +export class CollaborationUtils { + + @inject(CollaborationWorkspaceService) + protected readonly workspaceService: CollaborationWorkspaceService; + + getProtocolPath(uri?: URI): string | undefined { + if (!uri) { + return undefined; + } + const path = uri.path.toString(); + const roots = this.workspaceService.tryGetRoots(); + for (const root of roots) { + const rootUri = root.resource.path.toString() + '/'; + if (path.startsWith(rootUri)) { + return root.name + '/' + path.substring(rootUri.length); + } + } + return undefined; + } + + getResourceUri(path?: string): URI | undefined { + if (!path) { + return undefined; + } + const parts = path.split('/'); + const root = parts[0]; + const rest = parts.slice(1); + const stat = this.workspaceService.tryGetRoots().find(e => e.name === root); + if (stat) { + const uriPath = stat.resource.path.join(...rest); + const uri = stat.resource.withPath(uriPath); + return uri; + } else { + return undefined; + } + } + +} diff --git a/packages/collaboration/src/browser/collaboration-workspace-service.ts b/packages/collaboration/src/browser/collaboration-workspace-service.ts new file mode 100644 index 0000000000000..ac1cd59914e67 --- /dev/null +++ b/packages/collaboration/src/browser/collaboration-workspace-service.ts @@ -0,0 +1,69 @@ +// ***************************************************************************** +// 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 { nls } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; +import { FileStat } from '@theia/filesystem/lib/common/files'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { Workspace, ProtocolBroadcastConnection } from 'open-collaboration-protocol'; +import { CollaborationURI } from './collaboration-file-system-provider'; + +@injectable() +export class CollaborationWorkspaceService extends WorkspaceService { + + protected collabWorkspace?: Workspace; + protected connection?: ProtocolBroadcastConnection; + + async setHostWorkspace(workspace: Workspace, connection: ProtocolBroadcastConnection): Promise { + this.collabWorkspace = workspace; + this.connection = connection; + await this.setWorkspace({ + isDirectory: false, + isFile: true, + isReadonly: false, + isSymbolicLink: false, + name: nls.localize('theia/collaboration/collaborationWorkspace', 'Collaboration Workspace'), + resource: CollaborationURI.create(this.collabWorkspace) + }); + return Disposable.create(() => { + this.collabWorkspace = undefined; + this.connection = undefined; + this.setWorkspace(undefined); + }); + } + + protected override async computeRoots(): Promise { + if (this.collabWorkspace) { + return this.collabWorkspace.folders.map(e => this.entryToStat(e)); + } else { + return super.computeRoots(); + } + } + + protected entryToStat(entry: string): FileStat { + const uri = CollaborationURI.create(this.collabWorkspace!, entry); + return { + resource: uri, + name: entry, + isDirectory: true, + isFile: false, + isReadonly: false, + isSymbolicLink: false + }; + } + +} diff --git a/packages/collaboration/src/browser/style/index.css b/packages/collaboration/src/browser/style/index.css new file mode 100644 index 0000000000000..1d1eac50c03c8 --- /dev/null +++ b/packages/collaboration/src/browser/style/index.css @@ -0,0 +1,22 @@ +.theia-collaboration-selection-marker { + position: absolute; + content: " "; + border-right: solid 2px; + border-top: solid 2px; + border-bottom: solid 2px; + height: 100%; + box-sizing: border-box; +} + +.theia-collaboration-selection-marker::after { + position: absolute; + transform: translateY(-100%); + padding: 0 4px; + border-radius: 4px 4px 4px 0px; +} + +.theia-collaboration-selection-marker.theia-collaboration-selection-inverted::after { + transform: translateY(100%); + margin-top: -2px; + border-radius: 0px 4px 4px 4px; +} diff --git a/packages/collaboration/src/package.spec.ts b/packages/collaboration/src/package.spec.ts new file mode 100644 index 0000000000000..4e6f3abdcdccd --- /dev/null +++ b/packages/collaboration/src/package.spec.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('request package', () => { + + it('should support code coverage statistics', () => true); +}); diff --git a/packages/collaboration/tsconfig.json b/packages/collaboration/tsconfig.json new file mode 100644 index 0000000000000..5920c2dd0ba35 --- /dev/null +++ b/packages/collaboration/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + }, + { + "path": "../editor" + }, + { + "path": "../filesystem" + }, + { + "path": "../monaco" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/console/package.json b/packages/console/package.json index ac238ffa8396a..a0a68be4de510 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,12 +1,13 @@ { "name": "@theia/console", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Console Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/monaco": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "anser": "^2.0.1" + "@theia/core": "1.54.0", + "@theia/monaco": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", + "anser": "^2.0.1", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -41,7 +42,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/console/src/browser/console-widget.ts b/packages/console/src/browser/console-widget.ts index 0f6065fefde09..8948c9b57141b 100644 --- a/packages/console/src/browser/console-widget.ts +++ b/packages/console/src/browser/console-widget.ts @@ -139,7 +139,6 @@ export class ConsoleWidget extends BaseWidget implements StatefulWidget { input.getControl().createContextKey('consoleInputFocus', true); const contentContext = this.contextKeyService.createScoped(this.content.node); contentContext.setContext('consoleContentFocus', true); - this.toDispose.push(contentContext); } protected createInput(node: HTMLElement): Promise { diff --git a/packages/core/README.md b/packages/core/README.md index f7998ed224233..711ad6bab1baa 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -70,7 +70,7 @@ export class SomeClass { - `@theia/core/electron-shared/...` - `native-keymap` (from [`native-keymap@^2.2.1`](https://www.npmjs.com/package/native-keymap)) - - `electron` (from [`electron@^23.2.4`](https://www.npmjs.com/package/electron)) + - `electron` (from [`electron@^30.1.2`](https://www.npmjs.com/package/electron)) - `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store)) - `fix-path` (from [`fix-path@^3.0.0`](https://www.npmjs.com/package/fix-path)) - `@theia/core/shared/...` @@ -84,12 +84,12 @@ export class SomeClass { - `@phosphor/signaling` (from [`@phosphor/signaling@1`](https://www.npmjs.com/package/@phosphor/signaling)) - `@phosphor/virtualdom` (from [`@phosphor/virtualdom@1`](https://www.npmjs.com/package/@phosphor/virtualdom)) - `@phosphor/widgets` (from [`@phosphor/widgets@1`](https://www.npmjs.com/package/@phosphor/widgets)) - - `@theia/application-package` (from [`@theia/application-package@1.44.0`](https://www.npmjs.com/package/@theia/application-package/v/1.44.0)) - - `@theia/application-package/lib/api` (from [`@theia/application-package@1.44.0`](https://www.npmjs.com/package/@theia/application-package/v/1.44.0)) - - `@theia/application-package/lib/environment` (from [`@theia/application-package@1.44.0`](https://www.npmjs.com/package/@theia/application-package/v/1.44.0)) - - `@theia/request` (from [`@theia/request@1.44.0`](https://www.npmjs.com/package/@theia/request/v/1.44.0)) - - `@theia/request/lib/proxy` (from [`@theia/request@1.44.0`](https://www.npmjs.com/package/@theia/request/v/1.44.0)) - - `@theia/request/lib/node-request-service` (from [`@theia/request@1.44.0`](https://www.npmjs.com/package/@theia/request/v/1.44.0)) + - `@theia/application-package` (from [`@theia/application-package@1.54.0`](https://www.npmjs.com/package/@theia/application-package/v/1.54.0)) + - `@theia/application-package/lib/api` (from [`@theia/application-package@1.54.0`](https://www.npmjs.com/package/@theia/application-package/v/1.54.0)) + - `@theia/application-package/lib/environment` (from [`@theia/application-package@1.54.0`](https://www.npmjs.com/package/@theia/application-package/v/1.54.0)) + - `@theia/request` (from [`@theia/request@1.54.0`](https://www.npmjs.com/package/@theia/request/v/1.54.0)) + - `@theia/request/lib/proxy` (from [`@theia/request@1.54.0`](https://www.npmjs.com/package/@theia/request/v/1.54.0)) + - `@theia/request/lib/node-request-service` (from [`@theia/request@1.54.0`](https://www.npmjs.com/package/@theia/request/v/1.54.0)) - `fs-extra` (from [`fs-extra@^4.0.2`](https://www.npmjs.com/package/fs-extra)) - `fuzzy` (from [`fuzzy@^0.1.3`](https://www.npmjs.com/package/fuzzy)) - `inversify` (from [`inversify@^6.0.1`](https://www.npmjs.com/package/inversify)) @@ -98,14 +98,14 @@ export class SomeClass { - `react-virtuoso` (from [`react-virtuoso@^2.17.0`](https://www.npmjs.com/package/react-virtuoso)) - `vscode-languageserver-protocol` (from [`vscode-languageserver-protocol@^3.17.2`](https://www.npmjs.com/package/vscode-languageserver-protocol)) - `vscode-uri` (from [`vscode-uri@^2.1.1`](https://www.npmjs.com/package/vscode-uri)) + - `@parcel/watcher` (from [`@parcel/watcher@^2.4.1`](https://www.npmjs.com/package/@parcel/watcher)) - `dompurify` (from [`dompurify@^2.2.9`](https://www.npmjs.com/package/dompurify)) - - `express` (from [`express@^4.16.3`](https://www.npmjs.com/package/express)) + - `express` (from [`express@^4.21.0`](https://www.npmjs.com/package/express)) - `lodash.debounce` (from [`lodash.debounce@^4.0.8`](https://www.npmjs.com/package/lodash.debounce)) - `lodash.throttle` (from [`lodash.throttle@^4.1.1`](https://www.npmjs.com/package/lodash.throttle)) - - `nsfw` (from [`nsfw@^2.2.4`](https://www.npmjs.com/package/nsfw)) - `markdown-it` (from [`markdown-it@^12.3.2`](https://www.npmjs.com/package/markdown-it)) - `react` (from [`react@^18.2.0`](https://www.npmjs.com/package/react)) - - `ws` (from [`ws@^8.14.1`](https://www.npmjs.com/package/ws)) + - `ws` (from [`ws@^8.17.1`](https://www.npmjs.com/package/ws)) - `yargs` (from [`yargs@^15.3.1`](https://www.npmjs.com/package/yargs)) ## Logging Configuration @@ -138,8 +138,8 @@ existing loggers. However, each log message specifies from which logger it comes from, which can give an idea, without having to read the code: ``` -root INFO [nsfw-watcher: 10734] Started watching: /Users/captain.future/git/theia/CONTRIBUTING.md -^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^ +root INFO [parcel-watcher: 10734] Started watching: /Users/captain.future/git/theia/CONTRIBUTING.md +^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^ ``` Where `root` is the name of the logger and `INFO` is the log level. These are optionally followed by the name of a child process and the process ID. @@ -152,6 +152,10 @@ Where `root` is the name of the logger and `INFO` is the log level. These are op - If possible, you should set this environment variable: - When not set, Theia will allow any origin to access the WebSocket services. - When set, Theia will only allow the origins defined in this environment variable. +- `FRONTEND_CONNECTION_TIMEOUT` + - The duration in milliseconds during which the backend keeps the connection contexts for the frontend to reconnect. + - This duration defaults to '0' if not set. + - If set to negative number, the backend will never close the connection. ## Additional Information diff --git a/packages/core/README_TEMPLATE.md b/packages/core/README_TEMPLATE.md index 5b59b54820f49..76a0887aef570 100644 --- a/packages/core/README_TEMPLATE.md +++ b/packages/core/README_TEMPLATE.md @@ -107,8 +107,8 @@ existing loggers. However, each log message specifies from which logger it comes from, which can give an idea, without having to read the code: ``` -root INFO [nsfw-watcher: 10734] Started watching: /Users/captain.future/git/theia/CONTRIBUTING.md -^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^ +root INFO [parcel-watcher: 10734] Started watching: /Users/captain.future/git/theia/CONTRIBUTING.md +^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^ ``` Where `root` is the name of the logger and `INFO` is the log level. These are optionally followed by the name of a child process and the process ID. @@ -121,6 +121,10 @@ Where `root` is the name of the logger and `INFO` is the log level. These are op - If possible, you should set this environment variable: - When not set, Theia will allow any origin to access the WebSocket services. - When set, Theia will only allow the origins defined in this environment variable. +- `FRONTEND_CONNECTION_TIMEOUT` + - The duration in milliseconds during which the backend keeps the connection contexts for the frontend to reconnect. + - This duration defaults to '0' if not set. + - If set to negative number, the backend will never close the connection. ## Additional Information diff --git a/packages/core/i18n/nls.cs.json b/packages/core/i18n/nls.cs.json index 6f1c0b02a30a9..4d8ba688a5bc8 100644 --- a/packages/core/i18n/nls.cs.json +++ b/packages/core/i18n/nls.cs.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "Otevřete zobrazení konfigurace AI", + "aiHistory:open": "Otevřít zobrazení Historie AI", "debug.breakpoint.editCondition": "Upravit stav...", + "notebook.cell.changeToCode": "Změna buňky na kód", + "notebook.cell.changeToMarkdown": "Změna buňky na Mardown", + "notebook.cell.insertMarkdownCellAbove": "Vložení buňky Markdown nad", + "notebook.cell.insertMarkdownCellBelow": "Vložení buňky Markdown níže", "terminal:new:profile": "Vytvoření nového integrovaného terminálu z profilu", "terminal:profile:default": "Zvolte výchozí profil terminálu", "theia": { @@ -7,6 +13,33 @@ "noCallers": "Nebyl zjištěn žádný volající.", "open": "Hierarchie otevřených výzev" }, + "collaboration": { + "collaborate": "Spolupracujte", + "collaboration": "Spolupráce", + "collaborationWorkspace": "Pracovní prostor pro spolupráci", + "connected": "Připojeno", + "connectedSession": "Připojení k relaci spolupráce", + "copiedInvitation": "Kód pozvánky zkopírovaný do schránky.", + "copyAgain": "Znovu kopírovat", + "createRoom": "Vytvoření nové relace spolupráce", + "creatingRoom": "Vytvoření relace", + "end": "Ukončení relace spolupráce", + "endDetail": "Ukončit relaci, ukončit sdílení obsahu a zrušit přístup ostatním uživatelům.", + "enterCode": "Zadejte kód relace spolupráce", + "failedCreate": "Nepodařilo se vytvořit prostor: {0}", + "failedJoin": "Nepodařilo se připojit k místnosti: {0}", + "invite": "Pozvat ostatní", + "inviteDetail": "Zkopírujte si kód pozvánky, abyste ji mohli sdílet s ostatními a připojit se k sezení.", + "joinRoom": "Připojte se k zasedání o spolupráci", + "joiningRoom": "Připojení k relaci", + "leave": "Opustit zasedání pro spolupráci", + "leaveDetail": "Odpojte se od aktuální relace spolupráce a zavřete pracovní prostor.", + "selectCollaboration": "Vyberte možnost spolupráce", + "sharedSession": "Společná relace spolupráce", + "startSession": "Zahájení relace spolupráce nebo připojení se k ní", + "userWantsToJoin": "Uživatel '{0}' se chce připojit k místnosti pro spolupráci", + "whatToDo": "Co byste chtěli dělat s dalšími spolupracovníky?" + }, "core": { "about": { "compatibility": "{0} Kompatibilita", @@ -65,6 +98,13 @@ "next": "Další (dolů)", "previous": "Předchozí (Nahoru)" }, + "secondaryWindow": { + "alwaysOnTop": "Je-li tato funkce povolena, zůstane sekundární okno nad všemi ostatními okny, včetně oken různých aplikací.", + "description": "Nastaví počáteční pozici a velikost extrahovaného sekundárního okna.", + "fullSize": "Pozice a velikost extrahovaného widgetu bude stejná jako u spuštěné aplikace Theia.", + "halfWidth": "Pozice a velikost extrahovaného widgetu bude odpovídat polovině šířky spuštěné aplikace Theia.", + "originalSize": "Pozice a velikost extrahovaného widgetu bude stejná jako u původního widgetu." + }, "silentNotifications": "Řídí, zda se mají potlačit vyskakovací okna s oznámeními.", "tabDefaultSize": "Určuje výchozí velikost karet s ouškem.", "tabMaximize": "Ovládá, zda se mají karty maximalizovat při dvojím kliknutí.", @@ -94,14 +134,32 @@ "toggleTracing": "Povolení/zakázání sledování komunikace s ladicími adaptéry" }, "editor": { + "diffEditor.wordWrap2": "Řádky se budou obtékat podle nastavení `#editor.wordWrap#`.", "dirtyEncoding": "Soubor je znečištěný. Než jej znovu otevřete v jiném kódování, nejprve jej uložte.", - "editor.codeActionWidget.showHeaders": "Povolení/zakázání zobrazování záhlaví skupin v nabídce akcí kódu.", - "editor.experimental.pasteActions.enabled": "Povolení/zakázání spouštění úprav z rozšíření při vkládání.", + "editor.accessibilitySupport0": "Použití rozhraní API platformy ke zjištění, zda je připojena čtečka obrazovky.", + "editor.accessibilitySupport1": "Optimalizace pro použití se čtečkou obrazovky", + "editor.accessibilitySupport2": "Předpokládejme, že není připojena čtečka obrazovky", + "editor.bracketPairColorization.enabled": "Ovládá, zda je obarvení dvojice závorek povoleno, nebo ne. Pro přepsání barev zvýraznění závorek použijte `#workbench.colorCustomizations#`.", + "editor.codeActionWidget.includeNearbyQuickfixes": "Povolení/zakázání zobrazení nejbližší opravy v rámci řádku, pokud se zrovna neprovádí diagnostika.", + "editor.cursorSurroundingLinesStyle": "Řídí, kdy se má vynutit `#cursorSurroundingLines#`.", + "editor.detectIndentation": "Řídí, zda se při otevření souboru automaticky zjistí `#editor.tabSize#` a `#editor.insertSpaces#` na základě obsahu souboru.", + "editor.dropIntoEditor.enabled": "Ovládá, zda můžete soubor přetáhnout do textového editoru podržením klávesy `shift` (namísto otevření souboru v editoru).", "editor.formatOnSaveMode.modificationsIfAvailable": "Pokusí se formátovat pouze změny (vyžaduje kontrolu zdrojů). Pokud nelze použít kontrolu zdrojů, bude formátován celý soubor.", + "editor.hover.hidingDelay": "Řídí prodlevu v milisekundách, po které se hover skryje. Vyžaduje, aby bylo povoleno `editor.hover.sticky`.", "editor.inlayHints.enabled1": "Vložené nápovědy se zobrazují ve výchozím nastavení a skrývají se při podržení `Ctrl+Alt`", "editor.inlayHints.enabled2": "Vložené nápovědy jsou ve výchozím nastavení skryté a zobrazí se při podržení `Ctrl+Alt`.", + "editor.inlayHints.fontFamily": "Řídí rodinu písma vložených nápověd v editoru. Je-li nastaveno na prázdnou hodnotu, použije se `#editor.fontFamily#`.", + "editor.inlayHints.fontSize": "Ovládá velikost písma vložených nápověd v editoru. Ve výchozím nastavení se použije `#editor.fontSize#`, pokud je nastavená hodnota menší než `5` nebo větší než velikost písma editoru.", + "editor.insertSpaces": "Vkládání mezer při stisknutí `Tab`. Toto nastavení je přepsáno na základě obsahu souboru, pokud je zapnuta funkce `#editor.detectIndentation#`.", + "editor.occurrencesHighlight": "Řídí, zda má editor zvýrazňovat výskyty sémantických symbolů.", "editor.quickSuggestions": "Ovládá, zda se mají při psaní automaticky zobrazovat návrhy. To lze ovládat při psaní komentářů, řetězců a dalšího kódu. Rychlé návrhy lze nakonfigurovat tak, aby se zobrazovaly jako text ducha nebo s widgetem návrhu. Mějte také na paměti nastavení '#editor.suggestOnTriggerCharacters#', které řídí, zda se návrhy spouštějí pomocí speciálních znaků.", - "editor.suggest.matchOnWordStartOnly": "Je-li zapnuto filtrování IntelliSense, vyžaduje, aby první znak odpovídal začátku slova, např. `c` u `Console` nebo `WebContext`, ale _ne_ u `description`. Když je funkce IntelliSense vypnutá, zobrazí více výsledků, ale stále je třídí podle kvality shody.", + "editor.stickyScroll.scrollWithEditor": "Povolení posouvání widgetu sticky scroll pomocí horizontálního posuvníku editoru.", + "editor.suggestFontSize": "Velikost písma widgetu pro návrh. Je-li nastavena hodnota `0`, použije se hodnota `#editor.fontSize#`.", + "editor.suggestLineHeight": "Výška řádku pro widget návrhu. Je-li nastavena hodnota `0`, použije se hodnota `#editor.lineHeight#`. Minimální hodnota je 8.", + "editor.tabSize": "Počet mezer, kterým se rovná tabulátor. Toto nastavení je přepsáno na základě obsahu souboru, pokud je zapnuta funkce `#editor.detectIndentation#`.", + "editor.useTabStops": "Vkládání a odstraňování bílých znaků následuje po zarážkách tabulátoru.", + "editor.wordBasedSuggestions": "Řídí, zda se doplnění mají vypočítávat na základě slov v dokumentu.", + "editor.wordBasedSuggestionsMode": "Řídí, z jakých dokumentů se vypočítávají doplnění na základě slov.", "files.autoSave": "Ovládá [automatické ukládání](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) editorů, které mají neuložené změny.", "files.autoSave.afterDelay": "Editor se změnami se automaticky uloží po uplynutí nastavené doby `#files.autoSaveDelay#`.", "files.autoSave.off": "Editor se změnami se nikdy automaticky neuloží.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "před několika sekundami", "addSignedOff": "Přidat Signed-off-by", - "amendReuseMessag": "Chcete-li znovu použít poslední zprávu o revizi, stiskněte klávesu \"Enter\" nebo klávesu \"Escape\" pro zrušení.", + "amendReuseMessage": "Chcete-li znovu použít poslední zprávu o revizi, stiskněte klávesu \"Enter\" nebo klávesu \"Escape\" pro zrušení.", "amendRewrite": "Přepsání předchozí zprávy o revizi. Stisknutím klávesy 'Enter' potvrdíte nebo klávesou 'Escape' zrušíte.", "checkoutCreateLocalBranchWithName": "Vytvořte novou místní větev s názvem: {0}. Stiskněte klávesu \"Enter\" pro potvrzení nebo \"Escape\" pro zrušení.", "checkoutProvideBranchName": "Uveďte prosím název pobočky.", @@ -333,6 +391,9 @@ "alreadyRunning": "Hostovaná instance je již spuštěna.", "debugInstance": "Instance ladění", "debugMode": "Použití inspect nebo inspect-brk pro ladění Node.js", + "debugPorts": { + "debugPort": "Port, který se použije pro ladění Node.js tohoto serveru" + }, "devHost": "Vývojový hostitel", "failed": "Nepodařilo se spustit hostovanou instanci zásuvného modulu: {0}", "hostedPlugin": "Hostovaný plugin", @@ -364,6 +425,10 @@ "webviewTrace": "Řídí sledování komunikace s webovými pohledy.", "webviewWarnIfUnsecure": "Upozorňuje uživatele, že webové náhledy jsou v současné době nasazeny nezabezpečeně." }, + "preferences": { + "hostedPlugin": "Hostovaný plugin", + "toolbar": "Panel nástrojů" + }, "preview": { "openByDefault": "Ve výchozím nastavení otevřete místo editoru náhled." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "skryté", "config.untrackedChanges.mixed": "smíšené", "config.untrackedChanges.separate": "samostatné stránky", + "dirtyDiff": { + "close": "Zavřít Změnit pohled" + }, "history": "Historie", "noRepositoryFound": "Nebylo nalezeno žádné úložiště", "unamend": "Odstranit změny", @@ -401,8 +469,7 @@ "extract-widget": "Přesunutí zobrazení do sekundárního okna" }, "shell-area": { - "secondary": "Sekundární okno", - "top": "Nahoru" + "secondary": "Sekundární okno" }, "task": { "attachTask": "Připojte úkol...", @@ -422,6 +489,7 @@ "profilePath": "Cesta k shellu, který tento profil používá.", "profiles": "Profily, které se mají zobrazit při vytváření nového terminálu. Vlastnost path nastavte ručně pomocí nepovinných argumentů.\nNastavením existujícího profilu na `null` jej skryjete ze seznamu, například: `\"{0}\": null`.", "rendererType": "Ovládá způsob vykreslování terminálu.", + "rendererTypeDeprecationMessage": "Typ vykreslovače již není podporován jako volitelná možnost.", "selectProfile": "Výběr profilu pro nový terminál", "shell.deprecated": "Tento postup je zastaralý, nový doporučený způsob konfigurace výchozího shellu je vytvoření profilu terminálu v 'terminal.integrated.profiles.{0}' a nastavení jeho názvu jako výchozího v 'terminal.integrated.defaultProfile.{0}.'", "shellArgsLinux": "Argumenty příkazového řádku, které se použijí v terminálu Linuxu.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Zrušení všech testovacích běhů", + "stackFrameAt": "na adrese", "testRunDefaultName": "{0} spustit {1}", "testRuns": "Testovací běhy" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Hierarchie nadtypů" }, "vsx-registry": { + "confirmDialogMessage": "Rozšíření \"{0}\" není ověřeno a může představovat bezpečnostní riziko.", + "confirmDialogTitle": "Jste si jisti, že chcete pokračovat v instalaci ?", "downloadCount": "Počet stažení: {0}", "errorFetching": "Chyba při načítání rozšíření.", "errorFetchingConfigurationHint": "To může být způsobeno problémy s konfigurací sítě.", "failedInstallingVSIX": "Nepodařilo se nainstalovat {0} z VSIX.", "invalidVSIX": "Vybraný soubor není platný zásuvný modul \"*.vsix\".", "license": "Licence: {0}", + "onlyShowVerifiedExtensionsDescription": "Díky tomu se na stránkách {0} zobrazují pouze ověřená rozšíření.", + "onlyShowVerifiedExtensionsTitle": "Zobrazit pouze ověřená rozšíření", "recommendedExtensions": "Seznam názvů rozšíření doporučených pro použití v tomto pracovním prostoru.", "searchPlaceholder": "Hledat rozšíření v {0}", "showInstalled": "Zobrazit nainstalovaná rozšíření", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "Opravdu chcete odstranit všechny {0} vybrané soubory?", "confirmMessage.uriSingle": "Opravdu chcete odstranit {0}?", "duplicate": "Duplikát", - "failApply": "Nelze použít změny v novém souboru", "failSaveAs": "Nelze spustit \"{0}\" pro aktuální widget.", "newFilePlaceholder": "Název souboru", "newFolderPlaceholder": "Název složky", diff --git a/packages/core/i18n/nls.de.json b/packages/core/i18n/nls.de.json index ea9d6662ef5bf..99e7919a92af1 100644 --- a/packages/core/i18n/nls.de.json +++ b/packages/core/i18n/nls.de.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "AI-Konfigurationsansicht öffnen", + "aiHistory:open": "AI-Historienansicht öffnen", "debug.breakpoint.editCondition": "Edit Bedingung...", + "notebook.cell.changeToCode": "Zelle in Code ändern", + "notebook.cell.changeToMarkdown": "Zelle in Mardown ändern", + "notebook.cell.insertMarkdownCellAbove": "Markdown-Zelle oben einfügen", + "notebook.cell.insertMarkdownCellBelow": "Markdown-Zelle unten einfügen", "terminal:new:profile": "Neues integriertes Terminal aus einem Profil erstellen", "terminal:profile:default": "Wählen Sie das Standard-Terminalprofil", "theia": { @@ -7,6 +13,33 @@ "noCallers": "Es wurden keine Anrufer entdeckt.", "open": "Öffne Aufruf-Hierarchie" }, + "collaboration": { + "collaborate": "Zusammenarbeiten", + "collaboration": "Zusammenarbeit", + "collaborationWorkspace": "Arbeitsbereich für Zusammenarbeit", + "connected": "Verbunden", + "connectedSession": "Verbunden mit einer Sitzung zur Zusammenarbeit", + "copiedInvitation": "Einladungscode in die Zwischenablage kopiert.", + "copyAgain": "Erneut kopieren", + "createRoom": "Neue Collaboration-Sitzung erstellen", + "creatingRoom": "Sitzung erstellen", + "end": "Kollaborationssitzung beenden", + "endDetail": "Beenden Sie die Sitzung, beenden Sie die Freigabe von Inhalten und sperren Sie den Zugang für andere.", + "enterCode": "Enter collaboration session code", + "failedCreate": "Es konnte kein Raum erstellt werden: {0}", + "failedJoin": "Verbindung zum Raum fehlgeschlagen: {0}", + "invite": "Andere einladen", + "inviteDetail": "Kopieren Sie den Einladungscode, um ihn mit anderen zu teilen und an der Sitzung teilzunehmen.", + "joinRoom": "An der Collaboration-Sitzung teilnehmen", + "joiningRoom": "Beitrittssitzung", + "leave": "Kollaborationssitzung verlassen", + "leaveDetail": "Trennen Sie die Verbindung zur aktuellen Zusammenarbeitssitzung und schließen Sie den Arbeitsbereich.", + "selectCollaboration": "Wählen Sie die Option Zusammenarbeit", + "sharedSession": "Gemeinsame Sitzung zur Zusammenarbeit", + "startSession": "Zusammenarbeitssitzung starten oder beitreten", + "userWantsToJoin": "Benutzer '{0}' möchte dem Collaboration Room beitreten", + "whatToDo": "Was würden Sie gerne mit anderen Mitarbeitern machen?" + }, "core": { "about": { "compatibility": "{0} Kompatibilität", @@ -65,6 +98,13 @@ "next": "Nächste (Unten)", "previous": "Zurück (Oben)" }, + "secondaryWindow": { + "alwaysOnTop": "Wenn diese Funktion aktiviert ist, bleibt das sekundäre Fenster über allen anderen Fenstern, auch über denen anderer Anwendungen.", + "description": "Legt die Anfangsposition und -größe des extrahierten Sekundärfensters fest.", + "fullSize": "Die Position und Größe des extrahierten Widgets entspricht der der laufenden Theia-Anwendung.", + "halfWidth": "Die Position und Größe des extrahierten Widgets entspricht der halben Breite der laufenden Theia-Anwendung.", + "originalSize": "Position und Größe des extrahierten Widgets entsprechen denen des ursprünglichen Widgets." + }, "silentNotifications": "Legt fest, ob Benachrichtigungs-Popups unterdrückt werden sollen.", "tabDefaultSize": "Gibt die Standardgröße für Registerkarten an.", "tabMaximize": "Steuert, ob die Registerkarten bei einem Doppelklick maximiert werden sollen.", @@ -94,14 +134,32 @@ "toggleTracing": "Aktivieren/Deaktivieren der Verfolgung der Kommunikation mit Debug-Adaptern" }, "editor": { + "diffEditor.wordWrap2": "Der Zeilenumbruch erfolgt entsprechend der Einstellung `#editor.wordWrap#`.", "dirtyEncoding": "Die Datei ist verschmutzt. Bitte speichern Sie sie zuerst, bevor Sie sie mit einer anderen Kodierung erneut öffnen.", - "editor.codeActionWidget.showHeaders": "Aktivieren/deaktivieren Sie die Anzeige von Gruppenkopfzeilen im Code-Aktionsmenü.", - "editor.experimental.pasteActions.enabled": "Aktivieren/deaktivieren Sie laufende Bearbeitungen von Erweiterungen beim Einfügen.", + "editor.accessibilitySupport0": "Plattform-APIs verwenden, um zu erkennen, ob ein Screen Reader angeschlossen ist", + "editor.accessibilitySupport1": "Optimieren für die Nutzung mit einem Screen Reader", + "editor.accessibilitySupport2": "Angenommen, ein Bildschirmlesegerät ist nicht angeschlossen", + "editor.bracketPairColorization.enabled": "Steuert, ob die Einfärbung von Klammerpaaren aktiviert ist oder nicht. Verwenden Sie `#workbench.colorCustomizations#`, um die Farben der Klammerhervorhebung zu überschreiben.", + "editor.codeActionWidget.includeNearbyQuickfixes": "Aktivieren/Deaktivieren der Anzeige des nächstgelegenen Quickfix innerhalb einer Zeile, wenn keine Diagnose durchgeführt wird.", + "editor.cursorSurroundingLinesStyle": "Steuert, wann `#cursorSurroundingLines#` erzwungen werden soll.", + "editor.detectIndentation": "Steuert, ob `#editor.tabSize#` und `#editor.insertSpaces#` automatisch erkannt werden, wenn eine Datei geöffnet wird, basierend auf dem Inhalt der Datei.", + "editor.dropIntoEditor.enabled": "Steuert, ob Sie eine Datei durch Ziehen und Ablegen in einen Texteditor ziehen können, indem Sie die \"Umschalttaste\" gedrückt halten (anstatt die Datei in einem Editor zu öffnen).", "editor.formatOnSaveMode.modificationsIfAvailable": "Es wird versucht, nur Änderungen zu formatieren (erfordert Quellensicherung). Wenn die Versionskontrolle nicht verwendet werden kann, wird die gesamte Datei formatiert.", + "editor.hover.hidingDelay": "Steuert die Verzögerung in Millisekunden, nach der der Hover ausgeblendet wird. Erfordert, dass `editor.hover.sticky` aktiviert ist.", "editor.inlayHints.enabled1": "Inlay-Hinweise werden standardmäßig angezeigt und bei gedrückter Tastenkombination \"Strg+Alt\" ausgeblendet", "editor.inlayHints.enabled2": "Inlay-Hinweise sind standardmäßig ausgeblendet und werden angezeigt, wenn die Tastenkombination \"Strg+Alt\" gedrückt wird.", + "editor.inlayHints.fontFamily": "Steuert die Schriftfamilie der Inlay-Hinweise im Editor. Wenn leer, wird die `#editor.fontFamily#` verwendet.", + "editor.inlayHints.fontSize": "Steuert die Schriftgröße der Inlay-Hinweise im Editor. Als Vorgabe wird die `#editor.fontSize#` verwendet, wenn der konfigurierte Wert kleiner als `5` oder größer als die Editor-Schriftgröße ist.", + "editor.insertSpaces": "Leerzeichen einfügen, wenn `Tab` gedrückt wird. Diese Einstellung wird auf der Grundlage des Dateiinhalts außer Kraft gesetzt, wenn `#editor.detectIndentation#` eingeschaltet ist.", + "editor.occurrencesHighlight": "Steuert, ob der Editor das Auftreten von semantischen Symbolen hervorheben soll.", "editor.quickSuggestions": "Legt fest, ob während der Eingabe automatisch Vorschläge angezeigt werden sollen. Dies kann für die Eingabe von Kommentaren, Strings und anderem Code gesteuert werden. Schnellvorschläge können so konfiguriert werden, dass sie als Ghosttext oder mit dem Suggest-Widget angezeigt werden. Beachten Sie auch die '#editor.suggestOnTriggerCharacters#'-Einstellung, die steuert, ob Vorschläge durch Sonderzeichen ausgelöst werden.", - "editor.suggest.matchOnWordStartOnly": "Wenn die IntelliSense-Filterung aktiviert ist, muss das erste Zeichen am Wortanfang übereinstimmen, z. B. \"c\" bei \"Konsole\" oder \"Webkontext\", aber _nicht_ bei \"Beschreibung\". Wenn IntelliSense deaktiviert ist, werden mehr Ergebnisse angezeigt, aber immer noch nach der Qualität der Übereinstimmung sortiert.", + "editor.stickyScroll.scrollWithEditor": "Aktivieren Sie den Bildlauf des Sticky-Scroll-Widgets mit der horizontalen Bildlaufleiste des Editors.", + "editor.suggestFontSize": "Schriftgröße für das Vorschlags-Widget. Wenn auf `0` gesetzt, wird der Wert von `#editor.fontSize#` verwendet.", + "editor.suggestLineHeight": "Zeilenhöhe für das Vorschlags-Widget. Wenn auf `0` gesetzt, wird der Wert von `#editor.lineHeight#` verwendet. Der Mindestwert ist 8.", + "editor.tabSize": "Die Anzahl der Leerzeichen, die ein Tabulator ausmacht. Diese Einstellung wird basierend auf dem Inhalt der Datei überschrieben, wenn `#editor.detectIndentation#` eingeschaltet ist.", + "editor.useTabStops": "Das Einfügen und Löschen von Leerzeichen erfolgt nach Tabulatorstopps.", + "editor.wordBasedSuggestions": "Steuert, ob Vervollständigungen auf der Grundlage von Wörtern im Dokument berechnet werden sollen.", + "editor.wordBasedSuggestionsMode": "Steuert, aus welchen Dokumenten die wortbasierten Vervollständigungen berechnet werden.", "files.autoSave": "Steuert das [automatische Speichern](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) von Editoren, die ungespeicherte Änderungen haben.", "files.autoSave.afterDelay": "Ein Editor mit Änderungen wird automatisch nach der konfigurierten `#files.autoSaveDelay#` gespeichert.", "files.autoSave.off": "Ein Editor mit Änderungen wird nie automatisch gespeichert.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "vor ein paar Sekunden", "addSignedOff": "Abgezeichnet von hinzufügen", - "amendReuseMessag": "Um die letzte Meldung wieder zu verwenden, drücken Sie \"Enter\" oder \"Escape\", um den Vorgang abzubrechen.", + "amendReuseMessage": "Um die letzte Meldung wieder zu verwenden, drücken Sie \"Enter\" oder \"Escape\", um den Vorgang abzubrechen.", "amendRewrite": "Vorherige Übermittlungsnachricht neu schreiben. Bestätigen Sie mit \"Enter\" oder brechen Sie mit \"Escape\" ab.", "checkoutCreateLocalBranchWithName": "Erstellen Sie einen neuen lokalen Zweig mit dem Namen: {0}. Drücken Sie \"Enter\" zur Bestätigung oder \"Escape\" zum Abbrechen.", "checkoutProvideBranchName": "Bitte geben Sie den Namen einer Zweigstelle an.", @@ -333,6 +391,9 @@ "alreadyRunning": "Die gehostete Instanz läuft bereits.", "debugInstance": "Debug-Instanz", "debugMode": "Verwendung von inspect oder inspect-brk zur Fehlersuche in Node.js", + "debugPorts": { + "debugPort": "Zu verwendender Port für das Node.js-Debugging dieses Servers" + }, "devHost": "Entwicklung Host", "failed": "Die gehostete Plugin-Instanz konnte nicht ausgeführt werden: {0}", "hostedPlugin": "Gehostetes Plugin", @@ -364,6 +425,10 @@ "webviewTrace": "Steuert die Kommunikationsverfolgung mit Webviews.", "webviewWarnIfUnsecure": "Warnt Benutzer, dass Webviews derzeit unsicher eingesetzt werden." }, + "preferences": { + "hostedPlugin": "Gehostetes Plugin", + "toolbar": "Symbolleiste" + }, "preview": { "openByDefault": "Öffnen Sie standardmäßig die Vorschau anstelle des Editors." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "versteckt", "config.untrackedChanges.mixed": "gemischt", "config.untrackedChanges.separate": "getrennt", + "dirtyDiff": { + "close": "Schließen Ändern Peek-Ansicht" + }, "history": "Geschichte", "noRepositoryFound": "Kein Repository gefunden", "unamend": "Ändern", @@ -401,8 +469,7 @@ "extract-widget": "Ansicht in sekundäres Fenster verschieben" }, "shell-area": { - "secondary": "Sekundäres Fenster", - "top": "Top" + "secondary": "Sekundäres Fenster" }, "task": { "attachTask": "Aufgabe anhängen...", @@ -422,6 +489,7 @@ "profilePath": "Der Pfad der Shell, den dieses Profil benutzt.", "profiles": "Die Profile welche zur Erzeugung eines Terminals verwendet werden können. Setzen Sie den Pfad von Hand mit optionalen Parametern.\n\nSezen Sie ein Profile auf `null` um es zu verbergen, z.B.: `{0}: null`.", "rendererType": "Steuert, wie das Terminal gerendert wird.", + "rendererTypeDeprecationMessage": "Der Renderer-Typ wird nicht mehr als Option unterstützt.", "selectProfile": "Wählen Sie ein Profil für das neue Terminal", "shell.deprecated": "Dies ist veraltet, neu können Sie Ihre Shell konfigurieren, indem Sie ein Profil unter 'terminal.integrated.profiles.{0}' anlegen und dessen Namen in 'terminal.integrated.defaultProfile.{0}' als Standard setzen.", "shellArgsLinux": "Die Befehlszeilenargumente, die im Linux-Terminal zu verwenden sind.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Alle Testläufe abbrechen", + "stackFrameAt": "unter", "testRunDefaultName": "{0} laufen. {1}", "testRuns": "Testläufe" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Supertyp-Hierarchie" }, "vsx-registry": { + "confirmDialogMessage": "Die Erweiterung \"{0}\" ist ungeprüft und könnte ein Sicherheitsrisiko darstellen.", + "confirmDialogTitle": "Sind Sie sicher, dass Sie mit der Installation fortfahren wollen?", "downloadCount": "Anzahl der Downloads: {0}", "errorFetching": "Fehler beim Abrufen von Erweiterungen.", "errorFetchingConfigurationHint": "Dies könnte auf Probleme bei der Netzwerkkonfiguration zurückzuführen sein.", "failedInstallingVSIX": "Die Installation von {0} aus VSIX ist fehlgeschlagen.", "invalidVSIX": "Die ausgewählte Datei ist kein gültiges \"*.vsix\"-Plugin.", "license": "Lizenz: {0}", + "onlyShowVerifiedExtensionsDescription": "Auf diese Weise kann {0} nur verifizierte Durchwahlen anzeigen.", + "onlyShowVerifiedExtensionsTitle": "Nur geprüfte Erweiterungen anzeigen", "recommendedExtensions": "Eine Liste mit den Namen der Erweiterungen, die für die Verwendung in diesem Arbeitsbereich empfohlen werden.", "searchPlaceholder": "Erweiterungen suchen in {0}", "showInstalled": "Installierte Erweiterungen anzeigen", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "Wollen Sie wirklich alle {0} ausgewählten Dateien löschen?", "confirmMessage.uriSingle": "Wollen Sie wirklich {0} löschen?", "duplicate": "Duplizieren", - "failApply": "Änderungen konnten nicht auf neue Datei angewendet werden", "failSaveAs": "Kann \"{0}\" für das aktuelle Widget nicht ausführen.", "newFilePlaceholder": "File Name", "newFolderPlaceholder": "Folder Name", diff --git a/packages/core/i18n/nls.es.json b/packages/core/i18n/nls.es.json index 9b0018fc22319..bdc2b7c783ed9 100644 --- a/packages/core/i18n/nls.es.json +++ b/packages/core/i18n/nls.es.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "Abrir la vista Configuración AI", + "aiHistory:open": "Abrir la vista Historial de IA", "debug.breakpoint.editCondition": "Editar condición...", + "notebook.cell.changeToCode": "Cambiar celda por código", + "notebook.cell.changeToMarkdown": "Cambiar Celda a Mardown", + "notebook.cell.insertMarkdownCellAbove": "Insertar celda Markdown arriba", + "notebook.cell.insertMarkdownCellBelow": "Insertar celda Markdown abajo", "terminal:new:profile": "Crear un nuevo terminal integrado a partir de un perfil", "terminal:profile:default": "Elija el perfil de terminal por defecto", "theia": { @@ -7,6 +13,33 @@ "noCallers": "No se ha detectado ninguna llamada.", "open": "Jerarquía de la convocatoria abierta" }, + "collaboration": { + "collaborate": "Colabore", + "collaboration": "Colaboración", + "collaborationWorkspace": "Espacio de trabajo colaborativo", + "connected": "Conectado", + "connectedSession": "Conectado a una sesión de colaboración", + "copiedInvitation": "Código de invitación copiado en el portapapeles.", + "copyAgain": "Copiar de nuevo", + "createRoom": "Crear una nueva sesión de colaboración", + "creatingRoom": "Crear sesión", + "end": "Fin de la sesión de colaboración", + "endDetail": "Finalizar la sesión, dejar de compartir contenidos y revocar el acceso a otras personas.", + "enterCode": "Introduzca el código de la sesión de colaboración", + "failedCreate": "No se ha podido crear la sala: {0}", + "failedJoin": "No se ha podido entrar en la sala: {0}", + "invite": "Invitar a otros", + "inviteDetail": "Copie el código de invitación para compartirlo con otras personas y unirse a la sesión.", + "joinRoom": "Unirse a la sesión de colaboración", + "joiningRoom": "Sesión inaugural", + "leave": "Abandonar la sesión de colaboración", + "leaveDetail": "Desconectar de la sesión de colaboración actual y cerrar el espacio de trabajo.", + "selectCollaboration": "Seleccione la opción de colaboración", + "sharedSession": "Compartió una sesión de colaboración", + "startSession": "Iniciar o unirse a una sesión de colaboración", + "userWantsToJoin": "Usuario '{0}' quiere unirse a la sala de colaboración", + "whatToDo": "¿Qué le gustaría hacer con otros colaboradores?" + }, "core": { "about": { "compatibility": "{0} Compatibilidad", @@ -65,6 +98,13 @@ "next": "Siguiente (abajo)", "previous": "Anterior (Arriba)" }, + "secondaryWindow": { + "alwaysOnTop": "Cuando está activada, la ventana secundaria se mantiene por encima de todas las demás ventanas, incluidas las de distintas aplicaciones.", + "description": "Establece la posición inicial y el tamaño de la ventana secundaria extraída.", + "fullSize": "La posición y el tamaño del widget extraído serán los mismos que los de la aplicación Theia en ejecución.", + "halfWidth": "La posición y el tamaño del widget extraído serán la mitad de la anchura de la aplicación Theia en ejecución.", + "originalSize": "La posición y el tamaño del widget extraído serán los mismos que los del widget original." + }, "silentNotifications": "Controla si se suprimen las ventanas emergentes de notificación.", "tabDefaultSize": "Especifica el tamaño por defecto de las pestañas.", "tabMaximize": "Controla si se maximizan las pestañas al hacer doble clic.", @@ -94,14 +134,32 @@ "toggleTracing": "Activar/desactivar las comunicaciones de rastreo con los adaptadores de depuración" }, "editor": { + "diffEditor.wordWrap2": "Las líneas se ajustarán según la configuración de `#editor.wordWrap#`.", "dirtyEncoding": "El archivo está sucio. Por favor, guárdelo primero antes de volver a abrirlo con otra codificación.", - "editor.codeActionWidget.showHeaders": "Activar/desactivar la visualización de las cabeceras de los grupos en el menú de acción del código.", - "editor.experimental.pasteActions.enabled": "Activar/desactivar la ejecución de ediciones desde extensiones al pegar.", + "editor.accessibilitySupport0": "Utilizar las API de la plataforma para detectar si hay un lector de pantalla conectado.", + "editor.accessibilitySupport1": "Optimizar para el uso con un lector de pantalla", + "editor.accessibilitySupport2": "Supongamos que no hay un lector de pantalla conectado", + "editor.bracketPairColorization.enabled": "Controla si la coloración del par de corchetes está activada o no. Utilice `#workbench.colorCustomizations#` para anular los colores de resaltado de los corchetes.", + "editor.codeActionWidget.includeNearbyQuickfixes": "Activar/desactivar la visualización del quickfix más cercano dentro de una línea cuando no se está actualmente en un diagnóstico.", + "editor.cursorSurroundingLinesStyle": "Controla cuándo debe aplicarse `#cursorSurroundingLines#`.", + "editor.detectIndentation": "Controla si `#editor.tabSize#` y `#editor.insertSpaces#` se detectarán automáticamente al abrir un archivo en función de su contenido.", + "editor.dropIntoEditor.enabled": "Controla si puedes arrastrar y soltar un archivo en un editor de texto manteniendo pulsada la tecla `shift` (en lugar de abrir el archivo en un editor).", "editor.formatOnSaveMode.modificationsIfAvailable": "Intentará formatear sólo las modificaciones (requiere control de origen). Si no se puede utilizar el control de origen, se formateará todo el archivo.", + "editor.hover.hidingDelay": "Controla el retardo en milisegundos tras el cual el hover se oculta. Requiere que `editor.hover.sticky` esté activado.", "editor.inlayHints.enabled1": "Los consejos de incrustación se muestran por defecto y se ocultan cuando se mantiene `Ctrl+Alt`.", "editor.inlayHints.enabled2": "Los consejos de incrustación están ocultos por defecto y se muestran cuando se mantiene pulsado `Ctrl+Alt`.", + "editor.inlayHints.fontFamily": "Controla la familia de fuentes de las sugerencias de incrustación en el editor. Si está vacío, se utiliza `#editor.fontFamily#`.", + "editor.inlayHints.fontSize": "Controla el tamaño de la fuente de las sugerencias de incrustación en el editor. Por defecto se utiliza `#editor.fontSize#` cuando el valor configurado es menor que `5` o mayor que el tamaño de fuente del editor.", + "editor.insertSpaces": "Insertar espacios al pulsar `Tab`. Este ajuste se anula en función del contenido del archivo cuando `#editor.detectIndentation#` está activado.", + "editor.occurrencesHighlight": "Controla si el editor debe resaltar las apariciones de símbolos semánticos.", "editor.quickSuggestions": "Controla si las sugerencias deben aparecer automáticamente mientras se escribe. Esto puede controlarse para escribir comentarios, cadenas y otros códigos. La sugerencia rápida puede ser configurada para mostrarse como texto fantasma o con el widget de sugerencia. También hay que tener en cuenta la configuración '#editor.suggestOnTriggerCharacters#' que controla si las sugerencias son activadas por caracteres especiales.", - "editor.suggest.matchOnWordStartOnly": "Cuando se activa el filtro IntelliSense se requiere que el primer carácter coincida con el inicio de una palabra, por ejemplo `c` en `Console` o `WebContext` pero _no_ en `description`. Si se desactiva, IntelliSense mostrará más resultados, pero los ordenará según la calidad de la coincidencia.", + "editor.stickyScroll.scrollWithEditor": "Habilitar el desplazamiento del widget de desplazamiento pegajoso con la barra de desplazamiento horizontal del editor.", + "editor.suggestFontSize": "Tamaño de fuente para el widget de sugerencia. Cuando se establece en `0`, se utiliza el valor de `#editor.fontSize#`.", + "editor.suggestLineHeight": "Altura de línea para el widget de sugerencia. Cuando se establece en `0`, se utiliza el valor de `#editor.lineHeight#`. El valor mínimo es 8.", + "editor.tabSize": "El número de espacios a los que equivale un tabulador. Este ajuste se anula en función del contenido del archivo cuando `#editor.detectIndentation#` está activado.", + "editor.useTabStops": "Inserción y supresión de espacios en blanco después de los tabuladores.", + "editor.wordBasedSuggestions": "Controla si las terminaciones deben calcularse en función de las palabras del documento.", + "editor.wordBasedSuggestionsMode": "Controla a partir de qué documentos se calculan las terminaciones basadas en palabras.", "files.autoSave": "Controla el [autoguardado](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) de los editores que tienen cambios sin guardar.", "files.autoSave.afterDelay": "Un editor con cambios se guarda automáticamente después del `#files.autoSaveDelay#` configurado.", "files.autoSave.off": "Un editor con cambios nunca se guarda automáticamente.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "hace unos segundos", "addSignedOff": "Agregar a la lista de firmas", - "amendReuseMessag": "Para reutilizar el último mensaje de confirmación, pulse \"Enter\" o \"Escape\" para cancelar.", + "amendReuseMessage": "Para reutilizar el último mensaje de confirmación, pulse \"Enter\" o \"Escape\" para cancelar.", "amendRewrite": "Reescribir el mensaje de confirmación anterior. Pulse 'Enter' para confirmar o 'Escape' para cancelar.", "checkoutCreateLocalBranchWithName": "Cree una nueva sucursal local con el nombre: {0}. Pulse 'Enter' para confirmar o 'Escape' para cancelar.", "checkoutProvideBranchName": "Por favor, indique el nombre de la sucursal.", @@ -333,6 +391,9 @@ "alreadyRunning": "La instancia alojada ya está funcionando.", "debugInstance": "Instancia de depuración", "debugMode": "Uso de inspect o inspect-brk para la depuración de Node.js", + "debugPorts": { + "debugPort": "Puerto a usar para la depuración Node.js de este servidor" + }, "devHost": "Anfitrión del desarrollo", "failed": "Fallo en la ejecución de la instancia del plugin alojado: {0}", "hostedPlugin": "Plugin alojado", @@ -364,6 +425,10 @@ "webviewTrace": "Controla el rastreo de la comunicación con los webviews.", "webviewWarnIfUnsecure": "Advierte a los usuarios de que las vistas web se despliegan actualmente de forma insegura." }, + "preferences": { + "hostedPlugin": "Plugin alojado", + "toolbar": "Barra de herramientas" + }, "preview": { "openByDefault": "Abrir la vista previa en lugar del editor por defecto." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "oculto", "config.untrackedChanges.mixed": "mixto", "config.untrackedChanges.separate": "separar", + "dirtyDiff": { + "close": "Cerrar Cambiar Vista Peek" + }, "history": "Historia", "noRepositoryFound": "No se ha encontrado ningún repositorio", "unamend": "Sin modificar", @@ -401,8 +469,7 @@ "extract-widget": "Mover la vista a la ventana secundaria" }, "shell-area": { - "secondary": "Ventana secundaria", - "top": "Top" + "secondary": "Ventana secundaria" }, "task": { "attachTask": "Adjuntar tarea...", @@ -422,6 +489,7 @@ "profilePath": "La ruta del shell que utiliza este perfil.", "profiles": "Los perfiles a presentar cuando se crea un nuevo terminal. Establezca la propiedad path manualmente con argumentos opcionales.\nEstablezca un perfil existente como `null` para ocultar el perfil de la lista, por ejemplo: `\"{0}\": null`.", "rendererType": "Controla cómo se representa el terminal.", + "rendererTypeDeprecationMessage": "El tipo de renderizador ya no se admite como opción.", "selectProfile": "Seleccione un perfil para el nuevo terminal", "shell.deprecated": "Esto está obsoleto, la nueva forma recomendada de configurar tu shell por defecto es creando un perfil de terminal en 'terminal.integrated.profiles.{0}' y estableciendo su nombre de perfil como el predeterminado en 'terminal.integrated.defaultProfile.{0}.'", "shellArgsLinux": "Los argumentos de la línea de comandos a utilizar cuando en el terminal de Linux.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Cancelar todas las pruebas", + "stackFrameAt": "en", "testRunDefaultName": "{0} ejecute {1}", "testRuns": "Pruebas" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Jerarquía de supertipos" }, "vsx-registry": { + "confirmDialogMessage": "La extensión \"{0}\" no está verificada y puede suponer un riesgo para la seguridad.", + "confirmDialogTitle": "¿Está seguro de que desea continuar con la instalación?", "downloadCount": "Descargue el recuento: {0}", "errorFetching": "Error en la búsqueda de extensiones.", "errorFetchingConfigurationHint": "Esto podría deberse a problemas de configuración de la red.", "failedInstallingVSIX": "Fallo en la instalación de {0} desde VSIX.", "invalidVSIX": "El archivo seleccionado no es un plugin válido \"*.vsix\".", "license": "License: {0}", + "onlyShowVerifiedExtensionsDescription": "Esto permite que {0} sólo muestre extensiones verificadas.", + "onlyShowVerifiedExtensionsTitle": "Mostrar sólo extensiones verificadas", "recommendedExtensions": "Una lista de los nombres de las extensiones recomendadas para su uso en este espacio de trabajo.", "searchPlaceholder": "Buscar extensiones en {0}", "showInstalled": "Mostrar extensiones instaladas", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "¿Realmente quieres borrar todos los {0} archivos seleccionados?", "confirmMessage.uriSingle": "¿Realmente quieres borrar {0}?", "duplicate": "Duplicado", - "failApply": "No se han podido aplicar los cambios al nuevo archivo", "failSaveAs": "No se puede ejecutar \"{0}\" para el widget actual.", "newFilePlaceholder": "Nombre del archivo", "newFolderPlaceholder": "Nombre de la carpeta", diff --git a/packages/core/i18n/nls.fr.json b/packages/core/i18n/nls.fr.json index c5127822a80ab..b6685cda6431e 100644 --- a/packages/core/i18n/nls.fr.json +++ b/packages/core/i18n/nls.fr.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "Ouvrir la vue Configuration AI", + "aiHistory:open": "Ouvrir la vue de l'historique de l'IA", "debug.breakpoint.editCondition": "Edit Condition...", + "notebook.cell.changeToCode": "Changer la cellule en code", + "notebook.cell.changeToMarkdown": "Changer la cellule en Mardown", + "notebook.cell.insertMarkdownCellAbove": "Insérer une cellule Markdown au-dessus", + "notebook.cell.insertMarkdownCellBelow": "Insérer une cellule Markdown en dessous", "terminal:new:profile": "Créer un nouveau terminal intégré à partir d'un profil", "terminal:profile:default": "Choisissez le profil du terminal par défaut", "theia": { @@ -7,6 +13,33 @@ "noCallers": "Aucun appelant n'a été détecté.", "open": "Hiérarchie des appels ouverts" }, + "collaboration": { + "collaborate": "Collaborer", + "collaboration": "Collaboration", + "collaborationWorkspace": "Espace de travail collaboratif", + "connected": "Connecté", + "connectedSession": "Connecté à une session de collaboration", + "copiedInvitation": "Code d'invitation copié dans le presse-papiers.", + "copyAgain": "Copier à nouveau", + "createRoom": "Créer une nouvelle session de collaboration", + "creatingRoom": "Création d'une session", + "end": "Fin de la session de collaboration", + "endDetail": "Mettre fin à la session, cesser le partage de contenu et révoquer l'accès pour d'autres personnes.", + "enterCode": "Enter collaboration session code", + "failedCreate": "Échec de la création d'une salle : {0}", + "failedJoin": "N'a pas réussi à rejoindre la salle : {0}", + "invite": "Inviter d'autres personnes", + "inviteDetail": "Copiez le code d'invitation pour le partager avec d'autres personnes afin de participer à la session.", + "joinRoom": "Participer à une session de collaboration", + "joiningRoom": "Session d'adhésion", + "leave": "Quitter la session de collaboration", + "leaveDetail": "Se déconnecter de la session de collaboration en cours et fermer l'espace de travail.", + "selectCollaboration": "Sélectionner l'option de collaboration", + "sharedSession": "Partager une session de collaboration", + "startSession": "Démarrer ou rejoindre une session de collaboration", + "userWantsToJoin": "User '{0}' wants to join the collaboration room", + "whatToDo": "Qu'aimeriez-vous faire avec d'autres collaborateurs ?" + }, "core": { "about": { "compatibility": "{0} Compatibilité", @@ -65,6 +98,13 @@ "next": "Suivant (en bas)", "previous": "Précédent (en haut)" }, + "secondaryWindow": { + "alwaysOnTop": "Lorsqu'elle est activée, la fenêtre secondaire reste au-dessus de toutes les autres fenêtres, y compris celles des différentes applications.", + "description": "Définit la position et la taille initiales de la fenêtre secondaire extraite.", + "fullSize": "La position et la taille du widget extrait seront identiques à celles de l'application Theia en cours d'exécution.", + "halfWidth": "La position et la taille du widget extrait correspondront à la moitié de la largeur de l'application Theia en cours d'exécution.", + "originalSize": "La position et la taille du widget extrait seront identiques à celles du widget original." + }, "silentNotifications": "Contrôle la suppression des popups de notification.", "tabDefaultSize": "Spécifie la taille par défaut des onglets.", "tabMaximize": "Contrôle si les onglets doivent être maximisés lors d'un double-clic.", @@ -94,14 +134,32 @@ "toggleTracing": "Activer/désactiver le traçage des communications avec les adaptateurs de débogage" }, "editor": { + "diffEditor.wordWrap2": "Les lignes s'enrouleront en fonction du paramètre `#editor.wordWrap#`.", "dirtyEncoding": "Le fichier est sale. Veuillez le sauvegarder avant de le rouvrir avec un autre encodage.", - "editor.codeActionWidget.showHeaders": "Activez/désactivez l'affichage des en-têtes de groupe dans le menu d'action du code.", - "editor.experimental.pasteActions.enabled": "Activer/désactiver l'exécution des modifications des extensions lors du collage.", + "editor.accessibilitySupport0": "Utiliser les API de la plate-forme pour détecter la présence d'un lecteur d'écran.", + "editor.accessibilitySupport1": "Optimiser pour l'utilisation avec un lecteur d'écran", + "editor.accessibilitySupport2": "Supposons qu'il n'y ait pas de lecteur d'écran", + "editor.bracketPairColorization.enabled": "Contrôle si la colorisation des paires de crochets est activée ou non. Utilisez `#workbench.colorCustomizations#` pour surcharger les couleurs de mise en évidence des crochets.", + "editor.codeActionWidget.includeNearbyQuickfixes": "Activer/désactiver l'affichage de la réparation rapide la plus proche dans une ligne lorsqu'il n'y a pas de diagnostic en cours.", + "editor.cursorSurroundingLinesStyle": "Contrôle quand `#cursorSurroundingLines#` doit être appliqué.", + "editor.detectIndentation": "Contrôle si `#editor.tabSize#` et `#editor.insertSpaces#` seront automatiquement détectés lors de l'ouverture d'un fichier en fonction de son contenu.", + "editor.dropIntoEditor.enabled": "Contrôle si vous pouvez glisser-déposer un fichier dans un éditeur de texte en maintenant la touche \"Maj\" enfoncée (au lieu d'ouvrir le fichier dans un éditeur).", "editor.formatOnSaveMode.modificationsIfAvailable": "Tentera de formater uniquement les modifications (nécessite le contrôle de la source). Si le contrôle de la source ne peut pas être utilisé, alors le fichier entier sera formaté.", + "editor.hover.hidingDelay": "Contrôle le délai en millisecondes après lequel le survol est caché. Nécessite que `editor.hover.sticky` soit activé.", "editor.inlayHints.enabled1": "Les conseils d'incrustation sont affichés par défaut et sont masqués lorsque vous maintenez les touches `Ctrl+Alt`.", "editor.inlayHints.enabled2": "Les indices d'incrustation sont cachés par défaut et s'affichent en maintenant les touches `Ctrl+Alt`.", + "editor.inlayHints.fontFamily": "Contrôle la famille de police des indices d'incrustation dans l'éditeur. Si la valeur est vide, c'est la police `#editor.fontFamily#` qui est utilisée.", + "editor.inlayHints.fontSize": "Contrôle la taille de la police des indices d'incrustation dans l'éditeur. Par défaut, `#editor.fontSize#` est utilisé lorsque la valeur configurée est inférieure à `5` ou supérieure à la taille de la police de l'éditeur.", + "editor.insertSpaces": "Insérer des espaces lorsque l'on appuie sur `Tab`. Ce paramètre est modifié en fonction du contenu du fichier lorsque `#editor.detectIndentation#` est activé.", + "editor.occurrencesHighlight": "Contrôle si l'éditeur doit mettre en évidence les occurrences de symboles sémantiques.", "editor.quickSuggestions": "Contrôle si les suggestions doivent s'afficher automatiquement pendant la saisie. Cela peut être contrôlé pour la saisie de commentaires, de chaînes de caractères et d'autres codes. La suggestion rapide peut être configurée pour s'afficher sous forme de texte fantôme ou avec le widget de suggestion. Tenez également compte du paramètre '#editor.suggestOnTriggerCharacters#' qui contrôle si les suggestions sont déclenchées par des caractères spéciaux.", - "editor.suggest.matchOnWordStartOnly": "Lorsqu'il est activé, le filtrage IntelliSense exige que le premier caractère corresponde au début d'un mot, par exemple `c` sur `Console` ou `WebContext` mais _pas_ sur `description`. Lorsqu'il est désactivé, IntelliSense affiche plus de résultats mais les trie toujours par qualité de correspondance.", + "editor.stickyScroll.scrollWithEditor": "Active le défilement du widget de défilement collant avec la barre de défilement horizontale de l'éditeur.", + "editor.suggestFontSize": "Taille de la police pour le widget de suggestion. Si elle vaut `0`, la valeur de `#editor.fontSize#` est utilisée.", + "editor.suggestLineHeight": "Hauteur de ligne pour le widget de suggestion. Lorsqu'il vaut `0`, la valeur de `#editor.lineHeight#` est utilisée. La valeur minimale est de 8.", + "editor.tabSize": "Le nombre d'espaces que représente une tabulation. Ce paramètre est modifié en fonction du contenu du fichier lorsque `#editor.detectIndentation#` est activé.", + "editor.useTabStops": "L'insertion et la suppression d'espaces blancs suivent les taquets de tabulation.", + "editor.wordBasedSuggestions": "Contrôle si les compléments doivent être calculés sur la base des mots du document.", + "editor.wordBasedSuggestionsMode": "Contrôle les documents à partir desquels les complétions basées sur les mots sont calculées.", "files.autoSave": "Contrôle la [sauvegarde automatique](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) des éditeurs dont les modifications n'ont pas été sauvegardées.", "files.autoSave.afterDelay": "Un éditeur avec des modifications est automatiquement enregistré après le `#files.autoSaveDelay#` configuré.", "files.autoSave.off": "Un éditeur avec des modifications n'est jamais automatiquement sauvegardé.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "il y a quelques secondes", "addSignedOff": "Ajouter Signé-par", - "amendReuseMessag": "Pour réutiliser le dernier message de validation, appuyez sur 'Enter' ou 'Escape' pour annuler.", + "amendReuseMessage": "Pour réutiliser le dernier message de validation, appuyez sur 'Enter' ou 'Escape' pour annuler.", "amendRewrite": "Réécrire le message de livraison précédent. Appuyez sur 'Enter' pour confirmer ou 'Escape' pour annuler.", "checkoutCreateLocalBranchWithName": "Créez une nouvelle branche locale avec le nom : {0}. Appuyez sur 'Enter' pour confirmer ou 'Escape' pour annuler.", "checkoutProvideBranchName": "Veuillez indiquer le nom de la succursale.", @@ -333,6 +391,9 @@ "alreadyRunning": "L'instance hébergée est déjà en cours d'exécution.", "debugInstance": "Instance de débogage", "debugMode": "Utilisation de inspect ou inspect-brk pour le débogage de Node.js", + "debugPorts": { + "debugPort": "Port à utiliser pour le débogage Node.js de ce serveur" + }, "devHost": "Hôte de développement", "failed": "Échec de l'exécution de l'instance du plugin hébergé : {0}", "hostedPlugin": "Plugin hébergé", @@ -364,6 +425,10 @@ "webviewTrace": "Contrôle le traçage de la communication avec les webviews.", "webviewWarnIfUnsecure": "Avertit les utilisateurs que les webviews sont actuellement déployés de manière non sécurisée." }, + "preferences": { + "hostedPlugin": "Plugin hébergé", + "toolbar": "Barre d'outils" + }, "preview": { "openByDefault": "Ouvrir l'aperçu au lieu de l'éditeur par défaut." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "caché", "config.untrackedChanges.mixed": "mixte", "config.untrackedChanges.separate": "séparé", + "dirtyDiff": { + "close": "Fermer Modifier Regarder" + }, "history": "Histoire", "noRepositoryFound": "Aucun référentiel trouvé", "unamend": "Non modifié", @@ -401,8 +469,7 @@ "extract-widget": "Déplacer la vue vers une fenêtre secondaire" }, "shell-area": { - "secondary": "Fenêtre secondaire", - "top": "Haut" + "secondary": "Fenêtre secondaire" }, "task": { "attachTask": "Attacher la tâche...", @@ -422,6 +489,7 @@ "profilePath": "Le chemin du shell que ce profil utilise.", "profiles": "Les profils à présenter lors de la création d'un nouveau terminal. Définissez manuellement la propriété path avec des args optionnels.\nDonnez la valeur `null` à un profil existant pour le masquer dans la liste, par exemple : `\"{0}\" : null`.", "rendererType": "Contrôle la façon dont le terminal est rendu.", + "rendererTypeDeprecationMessage": "Le type de rendu n'est plus pris en charge en tant qu'option.", "selectProfile": "Sélectionnez un profil pour le nouveau terminal", "shell.deprecated": "Cette méthode est obsolète, la nouvelle méthode recommandée pour configurer votre shell par défaut est de créer un profil de terminal dans 'terminal.integrated.profiles.{0}' et de définir son nom de profil par défaut dans 'terminal.integrated.defaultProfile.{0}'.", "shellArgsLinux": "Les arguments de ligne de commande à utiliser dans le terminal Linux.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Annuler tous les essais", + "stackFrameAt": "à", "testRunDefaultName": "{0} courir {1}", "testRuns": "Essais" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Hiérarchie des supertypes" }, "vsx-registry": { + "confirmDialogMessage": "L'extension \"{0}\" n'est pas vérifiée et peut présenter un risque pour la sécurité.", + "confirmDialogTitle": "Êtes-vous sûr de vouloir procéder à l'installation ?", "downloadCount": "Compte de téléchargement : {0}", "errorFetching": "Erreur de récupération des extensions.", "errorFetchingConfigurationHint": "Cela peut être dû à des problèmes de configuration du réseau.", "failedInstallingVSIX": "Échec de l'installation de {0} à partir de VSIX.", "invalidVSIX": "Le fichier sélectionné n'est pas un plugin \"*.vsix\" valide.", "license": "Licence : {0}", + "onlyShowVerifiedExtensionsDescription": "Cela permet à {0} de n'afficher que les extensions vérifiées.", + "onlyShowVerifiedExtensionsTitle": "Afficher uniquement les extensions vérifiées", "recommendedExtensions": "Une liste des noms des extensions dont l'utilisation est recommandée dans cet espace de travail.", "searchPlaceholder": "Rechercher les extensions dans {0}", "showInstalled": "Afficher les extensions installées", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "Voulez-vous vraiment supprimer tous les {0} fichiers sélectionnés?", "confirmMessage.uriSingle": "Voulez-vous vraiment supprimer {0}?", "duplicate": "Duplicate", - "failApply": "Impossible d'appliquer les changements au nouveau fichier", "failSaveAs": "Impossible d'exécuter \"{0}\" pour le widget actuel.", "newFilePlaceholder": "Nom du fichier", "newFolderPlaceholder": "Nom du dossier", diff --git a/packages/core/i18n/nls.hu.json b/packages/core/i18n/nls.hu.json index 2dd14c9ac756b..c91fe4d959b39 100644 --- a/packages/core/i18n/nls.hu.json +++ b/packages/core/i18n/nls.hu.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "AI konfigurációs nézet megnyitása", + "aiHistory:open": "AI előzmények nézet megnyitása", "debug.breakpoint.editCondition": "Szerkesztési feltétel...", + "notebook.cell.changeToCode": "Cellát kódra váltani", + "notebook.cell.changeToMarkdown": "Cellát átváltoztatni Mardown-ra", + "notebook.cell.insertMarkdownCellAbove": "Markdown-cella beszúrása fent", + "notebook.cell.insertMarkdownCellBelow": "Markdown-cella beszúrása az alábbiakban", "terminal:new:profile": "Új integrált terminál létrehozása profilból", "terminal:profile:default": "Válassza ki az alapértelmezett terminálprofilt", "theia": { @@ -7,6 +13,33 @@ "noCallers": "Nem észleltek hívókat.", "open": "Nyílt felhívás hierarchia" }, + "collaboration": { + "collaborate": "Együttműködés", + "collaboration": "Együttműködés", + "collaborationWorkspace": "Együttműködési munkaterület", + "connected": "Csatlakoztatva", + "connectedSession": "Együttműködési munkamenethez csatlakoztatva", + "copiedInvitation": "Meghívó kódja a vágólapra másolva.", + "copyAgain": "Ismétlemásolás", + "createRoom": "Új együttműködési munkamenet létrehozása", + "creatingRoom": "Munkamenet létrehozása", + "end": "Együttműködési ülés befejezése", + "endDetail": "A munkamenet megszüntetése, a tartalom megosztásának megszüntetése és a hozzáférés visszavonása mások számára.", + "enterCode": "Adja meg az együttműködési munkamenet kódját", + "failedCreate": "Nem sikerült helyet létrehozni: {0}", + "failedJoin": "Nem sikerült csatlakozni a szobához: {0}", + "invite": "Mások meghívása", + "inviteDetail": "Másolja ki a meghívó kódját, hogy másokkal is megoszthassa, és csatlakozhasson az üléshez.", + "joinRoom": "Csatlakozzon az együttműködési üléshez", + "joiningRoom": "Csatlakozási munkamenet", + "leave": "Hagyja el az együttműködési ülést", + "leaveDetail": "Szakítsa meg a kapcsolatot az aktuális együttműködési munkamenetből, és zárja be a munkaterületet.", + "selectCollaboration": "Együttműködési lehetőség kiválasztása", + "sharedSession": "Közös együttműködési munkamenet", + "startSession": "Együttműködési munkamenet indítása vagy ahhoz való csatlakozás", + "userWantsToJoin": "Felhasználó '{0}' csatlakozni szeretne a kollaborációs szobához", + "whatToDo": "Mit szeretnél csinálni más munkatársakkal?" + }, "core": { "about": { "compatibility": "{0} Kompatibilitás", @@ -65,6 +98,13 @@ "next": "Következő (lefelé)", "previous": "Előző (fel)" }, + "secondaryWindow": { + "alwaysOnTop": "Ha engedélyezve van, a másodlagos ablak minden más ablak fölött marad, beleértve a különböző alkalmazások ablakait is.", + "description": "A kivont másodlagos ablak kezdeti pozíciójának és méretének beállítása.", + "fullSize": "A kivont widget pozíciója és mérete megegyezik a futó Theia alkalmazáséval.", + "halfWidth": "A kivont widget pozíciója és mérete a futó Theia alkalmazás szélességének fele lesz.", + "originalSize": "A kivont widget pozíciója és mérete megegyezik az eredeti widgetével." + }, "silentNotifications": "Beállítja, hogy az értesítések felugró ablakai el legyenek-e nyomva.", "tabDefaultSize": "Megadja a lapok alapértelmezett méretét.", "tabMaximize": "Szabályozza, hogy a lapok dupla kattintásra maximalizálódjanak-e.", @@ -94,14 +134,32 @@ "toggleTracing": "A hibakeresési adapterekkel folytatott kommunikáció nyomon követésének engedélyezése/letiltása" }, "editor": { + "diffEditor.wordWrap2": "A sorok a `#editor.wordWrap#` beállításnak megfelelően kerülnek a sorok közé.", "dirtyEncoding": "A fájl piszkos. Kérjük, először mentse el, mielőtt más kódolással újra megnyitná.", - "editor.codeActionWidget.showHeaders": "A csoportfejlécek megjelenítésének engedélyezése/letiltása a kódművelet menüben.", - "editor.experimental.pasteActions.enabled": "Beillesztéskor a bővítményekből származó szerkesztések futásának engedélyezése/letiltása.", + "editor.accessibilitySupport0": "Platform API-k használata a képernyőolvasó csatlakoztatásának észlelésére", + "editor.accessibilitySupport1": "Optimalizálja a képernyőolvasóval való használatra", + "editor.accessibilitySupport2": "Feltételezzük, hogy a képernyőolvasó nincs csatlakoztatva", + "editor.bracketPairColorization.enabled": "Szabályozza, hogy a zárójelpár színezése engedélyezve legyen-e vagy sem. Használja a `#workbench.colorCustomizations#` parancsot a zárójelek kiemelési színeinek felülbírálásához.", + "editor.codeActionWidget.includeNearbyQuickfixes": "A legközelebbi gyorsjavítás megjelenítésének engedélyezése/letiltása egy soron belül, ha éppen nem diagnosztikán van.", + "editor.cursorSurroundingLinesStyle": "Szabályozza, hogy mikor legyen érvényes a `#cursorSurroundingLines#`.", + "editor.detectIndentation": "Szabályozza, hogy a `#editor.tabSize#` és a `#editor.insertSpaces#` automatikusan felismerésre kerüljön-e egy fájl megnyitásakor a fájl tartalma alapján.", + "editor.dropIntoEditor.enabled": "Azt szabályozza, hogy a \"shift\" lenyomva tartásával húzhat-e egy fájlt egy szövegszerkesztőbe (ahelyett, hogy megnyitná a fájlt egy szerkesztőprogramban).", "editor.formatOnSaveMode.modificationsIfAvailable": "Csak a módosítások formázására tesz kísérletet (forrásellenőrzés szükséges). Ha a forrásellenőrzés nem használható, akkor a teljes fájl lesz formázva.", + "editor.hover.hidingDelay": "Ezredmásodpercben határozza meg a késleltetést, amely után a lebegő elrejtődik. A `editor.hover.sticky` engedélyezése szükséges.", "editor.inlayHints.enabled1": "Az inlay tippek alapértelmezés szerint megjelennek, és elrejtődnek, ha a \"Ctrl+Alt\" billentyűkombinációt nyomva tartjuk.", "editor.inlayHints.enabled2": "Az inlay tippek alapértelmezés szerint el vannak rejtve, és a \"Ctrl+Alt\" billentyűkombináció lenyomásakor jelennek meg.", + "editor.inlayHints.fontFamily": "A betűcsaládot vezérli a betétfájlok betűtípusát a szerkesztőben. Ha üres, akkor a `#editor.fontFamily#`-t használja.", + "editor.inlayHints.fontSize": "A betűméretet szabályozza a szerkesztőben megjelenő betűjelzések betűméretét. Alapértelmezés szerint az `#editor.fontSize#` értéket használja, ha a beállított érték kisebb, mint `5` vagy nagyobb, mint a szerkesztő betűmérete.", + "editor.insertSpaces": "Szóközök beillesztése a \"Tab\" billentyű lenyomásakor. Ez a beállítás a fájl tartalma alapján felülíródik, ha a `#editor.detectIndentation#` be van kapcsolva.", + "editor.occurrencesHighlight": "Szabályozza, hogy a szerkesztő kiemelje-e a szemantikus szimbólumok előfordulását.", "editor.quickSuggestions": "Szabályozza, hogy a javaslatok automatikusan megjelenjenek-e gépelés közben. Ezt a megjegyzések, karakterláncok és más kódok beírása esetén lehet szabályozni. A gyors javaslatok beállíthatók úgy, hogy szellemszövegként vagy a javaslat widget segítségével jelenjenek meg. Figyeljen a '#editor.suggestOnTriggerCharacters#'-beállításra is, amely azt szabályozza, hogy a javaslatok speciális karakterek hatására aktiválódjanak-e.", - "editor.suggest.matchOnWordStartOnly": "Ha engedélyezve van, az IntelliSense szűrés megköveteli, hogy az első karakter egyezzen a szó elején, pl. \"c\" a \"Konzol\" vagy a \"WebContext\" esetében, de _nem_ a \"leírás\" esetében. Ha kikapcsoljuk, az IntelliSense több eredményt mutat, de továbbra is az egyezés minősége szerint rendezi őket.", + "editor.stickyScroll.scrollWithEditor": "A ragadós görgető widget görgetésének engedélyezése a szerkesztő vízszintes görgetősávjával.", + "editor.suggestFontSize": "A javaslat widget betűmérete. Ha `0`-ra van állítva, akkor a `#editor.fontSize#` értékét használja.", + "editor.suggestLineHeight": "A javaslat widget vonalmagassága. Ha `0`-ra van állítva, akkor a `#editor.lineHeight#` értékét használja. A minimális érték 8.", + "editor.tabSize": "A tabulátor szóközök száma. Ez a beállítás a fájl tartalma alapján felülíródik, ha a `#editor.detectIndentation#` be van kapcsolva.", + "editor.useTabStops": "A szóközök beillesztése és törlése a tabulátormegállókat követi.", + "editor.wordBasedSuggestions": "Szabályozza, hogy a kiegészítések a dokumentumban található szavak alapján kerüljenek-e kiszámításra.", + "editor.wordBasedSuggestionsMode": "Azt vezérli, hogy mely dokumentumokból számolja ki a szóalapú kiegészítéseket.", "files.autoSave": "Az [automatikus mentés](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) vezérli a mentetlen módosításokkal rendelkező szerkesztőket.", "files.autoSave.afterDelay": "A szerkesztő a változtatásokat automatikusan elmenti a beállított `#files.autoSaveDelay#` után.", "files.autoSave.off": "Egy szerkesztő a módosításokkal soha nem kerül automatikusan mentésre.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "néhány másodperccel ezelőtt", "addSignedOff": "Signed-off-by hozzáadása", - "amendReuseMessag": "Az utolsó átadási üzenet újbóli használatához nyomja meg az 'Enter' billentyűt, vagy az 'Escape' billentyűt a törléshez.", + "amendReuseMessage": "Az utolsó átadási üzenet újbóli használatához nyomja meg az 'Enter' billentyűt, vagy az 'Escape' billentyűt a törléshez.", "amendRewrite": "Írja át az előző commit üzenetet. Nyomja meg az 'Enter' billentyűt a megerősítéshez vagy az 'Escape' billentyűt a törléshez.", "checkoutCreateLocalBranchWithName": "Hozzon létre egy új helyi ágat {0} névvel. Nyomja meg az 'Enter' billentyűt a megerősítéshez vagy az 'Escape' billentyűt a törléshez.", "checkoutProvideBranchName": "Kérjük, adja meg a fióktelep nevét.", @@ -333,6 +391,9 @@ "alreadyRunning": "A hosztolt példány már fut.", "debugInstance": "Hibakeresési példány", "debugMode": "Az inspect vagy inspect-brk használata a Node.js hibakereséshez", + "debugPorts": { + "debugPort": "A kiszolgáló Node.js hibakereséséhez használni kívánt port" + }, "devHost": "Fejlesztés Host", "failed": "Nem sikerült futtatni a hosztolt plugin példányt: {0}", "hostedPlugin": "Hostolt bővítmény", @@ -364,6 +425,10 @@ "webviewTrace": "Vezérli a kommunikáció nyomon követését a webnézetekkel.", "webviewWarnIfUnsecure": "Figyelmezteti a felhasználókat, hogy a webnézetek jelenleg nem biztonságosan vannak telepítve." }, + "preferences": { + "hostedPlugin": "Hostolt bővítmény", + "toolbar": "Eszköztár" + }, "preview": { "openByDefault": "Alapértelmezés szerint a szerkesztő helyett az előnézet megnyitása." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "rejtett", "config.untrackedChanges.mixed": "vegyes", "config.untrackedChanges.separate": "külön", + "dirtyDiff": { + "close": "Bezárás Változás Peek View" + }, "history": "Történelem", "noRepositoryFound": "Nem talált tárolóhely", "unamend": "Visszavonja a", @@ -401,8 +469,7 @@ "extract-widget": "Nézet áthelyezése másodlagos ablakba" }, "shell-area": { - "secondary": "Másodlagos ablak", - "top": "Tetejére" + "secondary": "Másodlagos ablak" }, "task": { "attachTask": "Feladat csatolása...", @@ -422,6 +489,7 @@ "profilePath": "A profil által használt héj elérési útja.", "profiles": "Az új terminál létrehozásakor megjelenítendő profilok. A path tulajdonság manuális beállítása opcionális args-ekkel.\nA meglévő profilok `null` értékre állítása a profil elrejtéséhez a listából, például: `\"{0}\": null`.", "rendererType": "A terminál megjelenítésének módját szabályozza.", + "rendererTypeDeprecationMessage": "A renderelő típusa már nem támogatott opció.", "selectProfile": "Válasszon ki egy profilt az új terminálhoz", "shell.deprecated": "Ez elavult, az új ajánlott módja az alapértelmezett shell konfigurálásának az, hogy létrehoz egy terminálprofilt a 'terminal.integrated.profiles.{0}' menüpontban, és beállítja a profil nevét alapértelmezettként a 'terminal.integrated.defaultProfile.{0}.' menüpontban.", "shellArgsLinux": "A Linux terminálon használandó parancssori argumentumok.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Minden tesztfuttatás törlése", + "stackFrameAt": "a címen.", "testRunDefaultName": "{0} fuss {1}", "testRuns": "Tesztfutások" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Szupertípus hierarchia" }, "vsx-registry": { + "confirmDialogMessage": "A \"{0}\" kiterjesztés nem ellenőrzött, és biztonsági kockázatot jelenthet.", + "confirmDialogTitle": "Biztos, hogy folytatni akarja a telepítést ?", "downloadCount": "Letöltési szám: {0}", "errorFetching": "Hiba a kiterjesztések lekérdezésében.", "errorFetchingConfigurationHint": "Ezt hálózati konfigurációs problémák okozhatják.", "failedInstallingVSIX": "Nem sikerült telepíteni a {0} oldalt a VSIX-ből.", "invalidVSIX": "A kiválasztott fájl nem érvényes \"*.vsix\" bővítmény.", "license": "Engedély: {0}", + "onlyShowVerifiedExtensionsDescription": "Ez lehetővé teszi, hogy a {0} csak ellenőrzött kiterjesztéseket jelenítsen meg.", + "onlyShowVerifiedExtensionsTitle": "Csak ellenőrzött kiterjesztések megjelenítése", "recommendedExtensions": "A munkaterületre ajánlott kiterjesztések neveinek listája.", "searchPlaceholder": "Keresés kiterjesztések {0}", "showInstalled": "Telepített bővítmények megjelenítése", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "Tényleg törölni szeretné az összes {0} kiválasztott fájlt?", "confirmMessage.uriSingle": "Tényleg törölni akarja a {0}-t?", "duplicate": "Duplikátum", - "failApply": "Nem tudta alkalmazni a változtatásokat az új fájlra", "failSaveAs": "Nem lehet futtatni a \"{0}\"-t az aktuális widgethez.", "newFilePlaceholder": "Fájlnév", "newFolderPlaceholder": "Mappa neve", diff --git a/packages/core/i18n/nls.it.json b/packages/core/i18n/nls.it.json index 6d8ecfe6a5372..41589e74f0a4c 100644 --- a/packages/core/i18n/nls.it.json +++ b/packages/core/i18n/nls.it.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "Aprire la vista Configurazione AI", + "aiHistory:open": "Aprire la vista Cronologia AI", "debug.breakpoint.editCondition": "Modifica della condizione...", + "notebook.cell.changeToCode": "Cambia cella in codice", + "notebook.cell.changeToMarkdown": "Cambiare la cella in Mardown", + "notebook.cell.insertMarkdownCellAbove": "Inserire la cella Markdown sopra", + "notebook.cell.insertMarkdownCellBelow": "Inserire la cella Markdown in basso", "terminal:new:profile": "Creare un nuovo terminale integrato da un profilo", "terminal:profile:default": "Scegliere il profilo del terminale predefinito", "theia": { @@ -7,6 +13,33 @@ "noCallers": "Non sono stati rilevati chiamanti.", "open": "Gerarchia delle chiamate aperte" }, + "collaboration": { + "collaborate": "Collaborate", + "collaboration": "Collaborazione", + "collaborationWorkspace": "Spazio di lavoro per la collaborazione", + "connected": "Collegato", + "connectedSession": "Collegato a una sessione di collaborazione", + "copiedInvitation": "Codice invito copiato negli appunti.", + "copyAgain": "Copia di nuovo", + "createRoom": "Creare una nuova sessione di collaborazione", + "creatingRoom": "Creazione di una sessione", + "end": "Fine della sessione di collaborazione", + "endDetail": "Terminare la sessione, interrompere la condivisione dei contenuti e revocare l'accesso ad altri.", + "enterCode": "Inserire il codice della sessione di collaborazione", + "failedCreate": "Impossibile creare una stanza: {0}", + "failedJoin": "Impossibile unirsi alla stanza: {0}", + "invite": "Invitare altri", + "inviteDetail": "Copiare il codice di invito per condividerlo con altri e partecipare alla sessione.", + "joinRoom": "Partecipa alla sessione di collaborazione", + "joiningRoom": "Sessione di adesione", + "leave": "Lasciare la sessione di collaborazione", + "leaveDetail": "Disconnettersi dalla sessione di collaborazione in corso e chiudere l'area di lavoro.", + "selectCollaboration": "Selezionare l'opzione di collaborazione", + "sharedSession": "Condivisione di una sessione di collaborazione", + "startSession": "Avviare o partecipare a una sessione di collaborazione", + "userWantsToJoin": "L'utente '{0}' vuole unirsi alla sala di collaborazione", + "whatToDo": "Cosa le piacerebbe fare con altri collaboratori?" + }, "core": { "about": { "compatibility": "{0} Compatibilità", @@ -65,6 +98,13 @@ "next": "Avanti (Giù)", "previous": "Precedente (Up)" }, + "secondaryWindow": { + "alwaysOnTop": "Quando è attivata, la finestra secondaria rimane al di sopra di tutte le altre finestre, comprese quelle di applicazioni diverse.", + "description": "Imposta la posizione e le dimensioni iniziali della finestra secondaria estratta.", + "fullSize": "La posizione e le dimensioni del widget estratto saranno le stesse dell'applicazione Theia in esecuzione.", + "halfWidth": "La posizione e le dimensioni del widget estratto saranno pari alla metà della larghezza dell'applicazione Theia in esecuzione.", + "originalSize": "La posizione e le dimensioni del widget estratto saranno uguali a quelle del widget originale." + }, "silentNotifications": "Controlla se sopprimere i popup di notifica.", "tabDefaultSize": "Specifica la dimensione predefinita delle schede.", "tabMaximize": "Controlla se massimizzare le schede al doppio clic.", @@ -94,14 +134,32 @@ "toggleTracing": "Abilita/disabilita il tracciamento delle comunicazioni con gli adattatori di debug" }, "editor": { + "diffEditor.wordWrap2": "Le righe andranno a capo secondo l'impostazione `#editor.wordWrap#`.", "dirtyEncoding": "Il file è sporco. Salvarlo prima di riaprirlo con un'altra codifica.", - "editor.codeActionWidget.showHeaders": "Abilita/disabilita la visualizzazione delle intestazioni dei gruppi nel menu delle azioni del codice.", - "editor.experimental.pasteActions.enabled": "Abilita/disabilita l'esecuzione di modifiche da parte delle estensioni in fase di incollaggio.", + "editor.accessibilitySupport0": "Utilizzare le API della piattaforma per rilevare la presenza di uno Screen Reader.", + "editor.accessibilitySupport1": "Ottimizzazione per l'utilizzo con uno screen reader", + "editor.accessibilitySupport2": "Supponiamo che un lettore di schermo non sia collegato", + "editor.bracketPairColorization.enabled": "Controlla se la colorazione delle coppie di parentesi è abilitata o meno. Usare `#workbench.colorCustomizations#` per sovrascrivere i colori di evidenziazione delle parentesi.", + "editor.codeActionWidget.includeNearbyQuickfixes": "Abilita/disabilita la visualizzazione della soluzione rapida più vicina all'interno di una linea quando non è in corso una diagnostica.", + "editor.cursorSurroundingLinesStyle": "Controlla quando `#cursorSurroundingLines#` deve essere applicato.", + "editor.detectIndentation": "Controlla se `#editor.tabSize#` e `#editor.insertSpaces#` saranno rilevati automaticamente all'apertura di un file, in base al suo contenuto.", + "editor.dropIntoEditor.enabled": "Controlla se è possibile trascinare e rilasciare un file in un editor di testo tenendo premuto `shift` (invece di aprire il file in un editor).", "editor.formatOnSaveMode.modificationsIfAvailable": "Tenterà di formattare solo le modifiche (richiede il controllo della fonte). Se il controllo della fonte non può essere usato, allora l'intero file sarà formattato.", + "editor.hover.hidingDelay": "Controlla il ritardo, in millisecondi, dopo il quale il passaggio del mouse viene nascosto. Richiede che `editor.hover.sticky` sia abilitato.", "editor.inlayHints.enabled1": "I suggerimenti per l'intarsio sono visualizzati per impostazione predefinita e si nascondono quando si tiene premuto `Ctrl+Alt`.", "editor.inlayHints.enabled2": "I suggerimenti per l'intarsio sono nascosti per impostazione predefinita e vengono visualizzati quando si tiene premuto `Ctrl+Alt`.", + "editor.inlayHints.fontFamily": "Controlla la famiglia di caratteri dei suggerimenti di inlay nell'editor. Se impostato a vuoto, viene utilizzata la `#editor.fontFamily#`.", + "editor.inlayHints.fontSize": "Controlla la dimensione dei caratteri dei suggerimenti inlay nell'editor. Come impostazione predefinita, viene utilizzato `#editor.fontSize#` quando il valore configurato è inferiore a `5` o superiore alla dimensione dei caratteri dell'editor.", + "editor.insertSpaces": "Inserisce spazi quando si preme `Tab`. Questa impostazione viene sovrascritta in base al contenuto del file quando `#editor.detectIndentation#` è attivo.", + "editor.occurrencesHighlight": "Controlla se l'editor deve evidenziare le occorrenze di simboli semantici.", "editor.quickSuggestions": "Controlla se i suggerimenti devono essere visualizzati automaticamente durante la digitazione. Questo può essere controllato per la digitazione di commenti, stringhe e altro codice. Il suggerimento rapido può essere configurato per essere visualizzato come testo fantasma o con il widget Suggerimento. Si tenga presente anche l'impostazione '#editor.suggestOnTriggerCharacters#', che controlla se i suggerimenti vengono attivati da caratteri speciali.", - "editor.suggest.matchOnWordStartOnly": "Se abilitato, il filtro IntelliSense richiede che il primo carattere corrisponda all'inizio di una parola, ad esempio `c` su `Console` o `WebContext` ma _non_ su `description`. Quando è disattivato, IntelliSense mostra più risultati, ma li ordina comunque in base alla qualità della corrispondenza.", + "editor.stickyScroll.scrollWithEditor": "Abilita lo scorrimento del widget di scorrimento appiccicoso con la barra di scorrimento orizzontale dell'editor.", + "editor.suggestFontSize": "Dimensione del carattere per il widget Suggerimento. Se impostato a `0`, viene utilizzato il valore di `#editor.fontSize#`.", + "editor.suggestLineHeight": "Altezza della riga per il widget di suggerimento. Se impostato a `0`, viene utilizzato il valore di `#editor.lineHeight#`. Il valore minimo è 8.", + "editor.tabSize": "Il numero di spazi a cui corrisponde una tabulazione. Questa impostazione viene sovrascritta in base al contenuto del file quando `#editor.detectIndentation#` è attivo.", + "editor.useTabStops": "L'inserimento e l'eliminazione di spazi bianchi segue gli stop di tabulazione.", + "editor.wordBasedSuggestions": "Controlla se i completamenti devono essere calcolati in base alle parole del documento.", + "editor.wordBasedSuggestionsMode": "Controlla da quali documenti vengono calcolati i completamenti basati sulle parole.", "files.autoSave": "Controlla il [salvataggio automatico](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) degli editor che hanno modifiche non salvate.", "files.autoSave.afterDelay": "Un editor con modifiche viene salvato automaticamente dopo il `#files.autoSaveDelay#` configurato.", "files.autoSave.off": "Un editor con modifiche non viene mai salvato automaticamente.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "pochi secondi fa", "addSignedOff": "Aggiungi Firmato-da", - "amendReuseMessag": "Per riutilizzare l'ultimo messaggio di commit, premi 'Enter' o 'Escape' per annullare.", + "amendReuseMessage": "Per riutilizzare l'ultimo messaggio di commit, premi 'Enter' o 'Escape' per annullare.", "amendRewrite": "Riscrivere il messaggio di commit precedente. Premi 'Enter' per confermare o 'Escape' per annullare.", "checkoutCreateLocalBranchWithName": "Crea un nuovo ramo locale con nome: {0}. Premi 'Enter' per confermare o 'Escape' per annullare.", "checkoutProvideBranchName": "Si prega di fornire il nome di una filiale.", @@ -333,6 +391,9 @@ "alreadyRunning": "L'istanza ospitata è già in esecuzione.", "debugInstance": "Istanza di debug", "debugMode": "Usare inspect o inspect-brk per il debug di Node.js", + "debugPorts": { + "debugPort": "Porta da utilizzare per il debug Node.js di questo server" + }, "devHost": "Sviluppo Host", "failed": "Impossibile eseguire l'istanza del plugin ospitato: {0}", "hostedPlugin": "Plugin ospitato", @@ -364,6 +425,10 @@ "webviewTrace": "Controlla la tracciabilità della comunicazione con le webview.", "webviewWarnIfUnsecure": "Avverte gli utenti che le webview sono attualmente distribuite in modo non sicuro." }, + "preferences": { + "hostedPlugin": "Plugin in hosting", + "toolbar": "Barra degli strumenti" + }, "preview": { "openByDefault": "Aprire l'anteprima invece dell'editor per default." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "nascosto", "config.untrackedChanges.mixed": "misto", "config.untrackedChanges.separate": "separato", + "dirtyDiff": { + "close": "Chiudi Cambia vista Peek" + }, "history": "Storia", "noRepositoryFound": "Nessun repository trovato", "unamend": "Unamend", @@ -401,8 +469,7 @@ "extract-widget": "Sposta la vista nella finestra secondaria" }, "shell-area": { - "secondary": "Finestra secondaria", - "top": "Alto" + "secondary": "Finestra secondaria" }, "task": { "attachTask": "Allegare il compito...", @@ -422,6 +489,7 @@ "profilePath": "Il percorso della shell utilizzata da questo profilo.", "profiles": "I profili da presentare quando si crea un nuovo terminale. Impostare manualmente la proprietà path con gli argomenti opzionali.\nImpostare un profilo esistente a `null` per nasconderlo dall'elenco, ad esempio: `\"{0}\": null`.", "rendererType": "Controlla il modo in cui il terminale viene reso.", + "rendererTypeDeprecationMessage": "Il tipo di renderer non è più supportato come opzione.", "selectProfile": "Selezionare un profilo per il nuovo terminale", "shell.deprecated": "Il nuovo modo consigliato per configurare la shell predefinita è quello di creare un profilo di terminale in 'terminal.integrated.profiles.{0}' e di impostare il nome del profilo come predefinito in 'terminal.integrated.defaultProfile.{0}'.", "shellArgsLinux": "Gli argomenti della riga di comando da usare quando si è sul terminale Linux.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Annullamento di tutte le esecuzioni di test", + "stackFrameAt": "a", "testRunDefaultName": "{0} corsa {1}", "testRuns": "Esecuzioni di prova" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Gerarchia dei supertipi" }, "vsx-registry": { + "confirmDialogMessage": "L'estensione \"{0}\" non è verificata e potrebbe rappresentare un rischio per la sicurezza.", + "confirmDialogTitle": "Siete sicuri di voler procedere con l'installazione?", "downloadCount": "Conteggio dei download: {0}", "errorFetching": "Errore nel recupero delle estensioni.", "errorFetchingConfigurationHint": "Questo potrebbe essere causato da problemi di configurazione della rete.", "failedInstallingVSIX": "Impossibile installare {0} da VSIX.", "invalidVSIX": "Il file selezionato non è un plugin \"*.vsix\" valido.", "license": "Licenza: {0}", + "onlyShowVerifiedExtensionsDescription": "Ciò consente a {0} di mostrare solo le estensioni verificate.", + "onlyShowVerifiedExtensionsTitle": "Mostra solo le estensioni verificate", "recommendedExtensions": "Una lista dei nomi delle estensioni raccomandate per l'uso in questo spazio di lavoro.", "searchPlaceholder": "Estensioni di ricerca in {0}", "showInstalled": "Mostra le estensioni installate", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "Volete davvero eliminare tutti i {0} file selezionati?", "confirmMessage.uriSingle": "Si vuole davvero cancellare {0}?", "duplicate": "Duplicato", - "failApply": "Impossibile applicare le modifiche al nuovo file", "failSaveAs": "Impossibile eseguire \"{0}\" per il widget corrente.", "newFilePlaceholder": "Nome del file", "newFolderPlaceholder": "Nome della cartella", diff --git a/packages/core/i18n/nls.ja.json b/packages/core/i18n/nls.ja.json index 4d5f59f5dc7f8..ddcbf18d4c7a2 100644 --- a/packages/core/i18n/nls.ja.json +++ b/packages/core/i18n/nls.ja.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "AI設定ビューを開く", + "aiHistory:open": "AI履歴ビューを開く", "debug.breakpoint.editCondition": "編集条件...", + "notebook.cell.changeToCode": "セルをコードに変更", + "notebook.cell.changeToMarkdown": "セルをマーダウンに変更", + "notebook.cell.insertMarkdownCellAbove": "マークダウン・セルを上に挿入する", + "notebook.cell.insertMarkdownCellBelow": "下にマークダウン・セルを挿入する", "terminal:new:profile": "プロファイルから新しい統合端末を作成する", "terminal:profile:default": "デフォルトの端末プロファイルを選択", "theia": { @@ -7,6 +13,33 @@ "noCallers": "発信者は検出されていません。", "open": "オープンコールヒエラルキー" }, + "collaboration": { + "collaborate": "コラボレーション", + "collaboration": "コラボレーション", + "collaborationWorkspace": "コラボレーション・ワークスペース", + "connected": "接続済み", + "connectedSession": "コラボレーション・セッションに接続", + "copiedInvitation": "招待状コードがクリップボードにコピーされました。", + "copyAgain": "コピー・アゲイン", + "createRoom": "新しいコラボレーション・セッションの作成", + "creatingRoom": "セッションの作成", + "end": "コラボレーション・セッション終了", + "endDetail": "セッションを終了し、コンテンツの共有を停止し、他の人のアクセスを取り消す。", + "enterCode": "コラボレーションセッションコードを入力", + "failedCreate": "部屋の作成に失敗しました:{0}", + "failedJoin": "入室に失敗:{0}", + "invite": "他の人を招待する", + "inviteDetail": "招待コードをコピーして他の人と共有し、セッションに参加してください。", + "joinRoom": "コラボレーション・セッションに参加", + "joiningRoom": "セッションへの参加", + "leave": "コラボレーション・セッション", + "leaveDetail": "現在のコラボレーションセッションから切断し、ワークスペースを閉じます。", + "selectCollaboration": "コラボレーション・オプションを選択", + "sharedSession": "コラボレーション・セッションを共有", + "startSession": "コラボレーションセッションの開始または参加", + "userWantsToJoin": "ユーザー '{0}' がコラボレーションルームへの参加を希望しています。", + "whatToDo": "他の協力者とどんなことをしたいですか?" + }, "core": { "about": { "compatibility": "{0} 互換性", @@ -65,6 +98,13 @@ "next": "次へ(下)", "previous": "前へ (上)" }, + "secondaryWindow": { + "alwaysOnTop": "有効にすると、セカンダリウィンドウは、異なるアプリケーションを含む他のすべてのウィンドウの上に表示されます。", + "description": "抽出されたセカンダリウィンドウの初期位置とサイズを設定する。", + "fullSize": "抽出されたウィジェットの位置とサイズは、実行中のTheiaアプリケーションと同じになります。", + "halfWidth": "抽出されたウィジェットの位置とサイズは、実行中のTheiaアプリケーションの幅の半分になります。", + "originalSize": "抽出されたウィジェットの位置とサイズは、元のウィジェットと同じになる。" + }, "silentNotifications": "通知のポップアップを抑制するかどうかを制御します。", "tabDefaultSize": "タブのデフォルトサイズを指定します。", "tabMaximize": "ダブルクリック時にタブを最大化するかどうかを制御します。", @@ -94,14 +134,32 @@ "toggleTracing": "デバッグアダプタとの通信のトレースの有効化/無効化" }, "editor": { + "diffEditor.wordWrap2": "行は `#editor.wordWrap#` の設定に従って折り返されます。", "dirtyEncoding": "ファイルが汚れています。別のエンコーディングで開き直す前に、まず保存してください。", - "editor.codeActionWidget.showHeaders": "コードアクションメニューにグループヘッダを表示するかどうかを設定します。", - "editor.experimental.pasteActions.enabled": "ペースト時に拡張機能からの編集を実行するかどうかを設定します。", + "editor.accessibilitySupport0": "プラットフォームAPIを使用して、スクリーンリーダーが接続されていることを検出する。", + "editor.accessibilitySupport1": "スクリーンリーダーでの使用に最適化する", + "editor.accessibilitySupport2": "スクリーンリーダーが添付されていないと仮定する", + "editor.bracketPairColorization.enabled": "ブラケットペアのカラー化を有効にするかどうかを制御する。ブラケットのハイライト色を上書きするには `#workbench.colorCustomizations#` を使用してください。", + "editor.codeActionWidget.includeNearbyQuickfixes": "現在診断中でない場合に、行内に最も近いクイックフィックスを表示するかどうかを設定します。", + "editor.cursorSurroundingLinesStyle": "cursorSurroundingLines#`を実行するタイミングを制御する。", + "editor.detectIndentation": "ファイルを開いたときに、ファイルの内容に基づいて `#editor.tabSize#` と `#editor.insertSpaces#` を自動的に検出するかどうかを制御する。", + "editor.dropIntoEditor.enabled": "shift`を押しながらファイルをテキストエディタにドラッグ&ドロップできるかどうかをコントロールします(エディタでファイルを開く代わりに)。", "editor.formatOnSaveMode.modificationsIfAvailable": "変更点のみをフォーマットしようとします(ソースコントロールが必要です)。ソースコントロールが使用できない場合は、ファイル全体がフォーマットされます。", + "editor.hover.hidingDelay": "ホバーが非表示になるまでの遅延をミリ秒単位で制御する。editor.hover.sticky`が有効になっている必要があります。", "editor.inlayHints.enabled1": "インレイのヒントはデフォルトで表示され、`Ctrl+Alt`を押すと隠れます。", "editor.inlayHints.enabled2": "インレイのヒントはデフォルトでは非表示で、`Ctrl+Alt`を押したときに表示されます。", + "editor.inlayHints.fontFamily": "エディタのインレイヒントのフォントファミリーを制御する。空に設定すると、`#editor.fontFamily#`が使用されます。", + "editor.inlayHints.fontSize": "エディタのインレイヒントのフォントサイズを制御する。デフォルトでは`#editor.fontSize#`が使用され、設定値が`5`未満またはエディタのフォントサイズより大きい場合に使用されます。", + "editor.insertSpaces": "Tab`を押したときにスペースを挿入する。この設定は、`#editor.detectIndentation#`がオンの場合、ファイルの内容によって上書きされる。", + "editor.occurrencesHighlight": "エディターが意味記号の出現をハイライトするかどうかを制御する。", "editor.quickSuggestions": "入力中に自動的にサジェストを表示するかどうかを制御します。コメント、文字列、その他のコードを入力する際に制御できます。クイックサジェストは、ゴーストテキストとして表示するか、サジェストウィジェットで表示するかを設定できます。また、'#editor.suggestOnTriggerCharacters#'設定は、特殊な文字でサジェストが発生するかどうかを制御します。", - "editor.suggest.matchOnWordStartOnly": "インテリセンスのフィルタリングを有効にすると、最初の文字が単語の先頭にマッチする必要があります。例えば、`Console` や `WebContext` では `c` ですが、`description` では _not_ です。無効化すると、インテリセンスはより多くの結果を表示しますが、マッチの質でソートされます。", + "editor.stickyScroll.scrollWithEditor": "エディターの水平スクロールバーでスティッキースクロールウィジェットのスクロールを有効にする。", + "editor.suggestFontSize": "サジェストウィジェットのフォントサイズ。0`に設定すると、`#editor.fontSize#`の値が使用される。", + "editor.suggestLineHeight": "提案ウィジェットの行の高さ。0`に設定すると、`#editor.lineHeight#`の値が使用される。最小値は8。", + "editor.tabSize": "タブのスペース数。この設定は、`#editor.detectIndentation#`がオンの場合、ファイルの内容によって上書きされる。", + "editor.useTabStops": "空白の挿入と削除はタブストップに従う。", + "editor.wordBasedSuggestions": "文書内の単語に基づいて補完を計算するかどうかを制御する。", + "editor.wordBasedSuggestionsMode": "どの文書から単語ベースの補完を計算するかを制御する。", "files.autoSave": "未保存の変更があるエディターの[自動保存](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save)を制御します。", "files.autoSave.afterDelay": "変更されたエディターは、設定された `#files.autoSaveDelay#` の後に自動的に保存されます。", "files.autoSave.off": "変更されたエディタは、自動的に保存されることはありません。", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "さきほど", "addSignedOff": "サイン・オフ・バイの追加", - "amendReuseMessag": "最後のコミットメッセージを再利用するには、「Enter」または「Escape」を押してキャンセルしてください。", + "amendReuseMessage": "最後のコミットメッセージを再利用するには、「Enter」または「Escape」を押してキャンセルしてください。", "amendRewrite": "前回のコミットメッセージを書き換えます。確定するには「Enter」を、キャンセルするには「Escape」を押してください。", "checkoutCreateLocalBranchWithName": "名前:{0}で新しいローカルブランチを作成します。確定するには「Enter」を、キャンセルするには「Escape」を押してください。", "checkoutProvideBranchName": "支店名をご記入ください。", @@ -333,6 +391,9 @@ "alreadyRunning": "ホストされているインスタンスはすでに実行されています。", "debugInstance": "デバッグインスタンス", "debugMode": "Node.js のデバッグに inspect または inspect-brk を使用する", + "debugPorts": { + "debugPort": "このサーバーのNode.jsデバッグに使用するポート。" + }, "devHost": "開発ホスト", "failed": "ホストされたプラグインインスタンスの実行に失敗しました。{0}", "hostedPlugin": "ホスト型プラグイン", @@ -364,6 +425,10 @@ "webviewTrace": "ウェブビューによる通信のトレースを制御します。", "webviewWarnIfUnsecure": "現在、ウェブビューが安全でない状態で展開されていることをユーザーに警告します。" }, + "preferences": { + "hostedPlugin": "ホスト型プラグイン", + "toolbar": "ツールバー" + }, "preview": { "openByDefault": "デフォルトではエディタではなくプレビューを開くようになっています。" }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "隠れ", "config.untrackedChanges.mixed": "合成", "config.untrackedChanges.separate": "別", + "dirtyDiff": { + "close": "クローズチェンジ・ピーキング・ビュー" + }, "history": "歴史", "noRepositoryFound": "リポジトリが見つからない", "unamend": "くり返す", @@ -401,8 +469,7 @@ "extract-widget": "セカンダリーウィンドウへの表示移動" }, "shell-area": { - "secondary": "セカンダリーウィンドウ", - "top": "トップ" + "secondary": "セカンダリーウィンドウ" }, "task": { "attachTask": "タスクの添付...", @@ -422,6 +489,7 @@ "profilePath": "このプロファイルが使用するシェルのパス。", "profiles": "新しいターミナルを作成するときに提示するプロファイル。オプションのアーギュメントを使用して、手動でパスプロパティを設定します。\n既存のプロファイルを `null` に設定すると、リストからプロファイルを隠すことができます。 例: `\"{0}\": null`.", "rendererType": "端末のレンダリング方法を制御する。", + "rendererTypeDeprecationMessage": "レンダラータイプはオプションとしてサポートされなくなった。", "selectProfile": "新規端末のプロファイルを選択する", "shell.deprecated": "これは非推奨です。デフォルトシェルを設定する新しい推奨方法は、 'terminal.integrated.profiles.{0}' でターミナルプロファイルを作成し、 'terminal.integrated.defaultProfile.{0}.' でそのプロファイル名をデフォルトとして設定する方法です。", "shellArgsLinux": "Linux端末で使用するコマンドライン引数です。", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "すべてのテスト実行をキャンセルする", + "stackFrameAt": "で", "testRunDefaultName": "{0} 走る{1}", "testRuns": "テスト走行" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "スーパータイプヒエラルキー" }, "vsx-registry": { + "confirmDialogMessage": "拡張子 \"{0}\" は未検証であり、セキュリティリスクをもたらす可能性があります。", + "confirmDialogTitle": "本当にインストールを続行しますか?", "downloadCount": "ダウンロード回数{0}", "errorFetching": "拡張機能の取得にエラーが発生しました。", "errorFetchingConfigurationHint": "これは、ネットワーク設定の問題が原因である可能性がある。", "failedInstallingVSIX": "VSIXから{0} のインストールに失敗しました。", "invalidVSIX": "選択されたファイルは、有効な \"*.vsix \"プラグインではありません。", "license": "ライセンス{0}", + "onlyShowVerifiedExtensionsDescription": "これにより、{0} 、検証済みの拡張子のみを表示することができる。", + "onlyShowVerifiedExtensionsTitle": "検証済みのエクステンションのみ表示", "recommendedExtensions": "このワークスペースでの使用が推奨される拡張機能の名前のリストです。", "searchPlaceholder": "{0}で拡張子を検索", "showInstalled": "インストールされている拡張機能を表示する", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "本当に{0}個の選択されたファイルをすべて削除しますか?", "confirmMessage.uriSingle": "本当に{0}を削除するのですか?", "duplicate": "デュプリケート", - "failApply": "新しいファイルに変更を適用できなかった", "failSaveAs": "現在のウィジェットでは\"{0}\"を実行できません。", "newFilePlaceholder": "ファイル名", "newFolderPlaceholder": "フォルダー名", diff --git a/packages/core/i18n/nls.json b/packages/core/i18n/nls.json index 0405d836b3aad..e20a2fba22fcc 100644 --- a/packages/core/i18n/nls.json +++ b/packages/core/i18n/nls.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "Open AI Configuration view", + "aiHistory:open": "Open AI History view", "debug.breakpoint.editCondition": "Edit Condition...", + "notebook.cell.changeToCode": "Change Cell to Code", + "notebook.cell.changeToMarkdown": "Change Cell to Markdown", + "notebook.cell.insertMarkdownCellAbove": "Insert Markdown Cell Above", + "notebook.cell.insertMarkdownCellBelow": "Insert Markdown Cell Below", "terminal:new:profile": "Create New Integrated Terminal from a Profile", "terminal:profile:default": "Choose the default Terminal Profile", "theia": { @@ -7,6 +13,33 @@ "noCallers": "No callers have been detected.", "open": "Open Call Hierarchy" }, + "collaboration": { + "collaborate": "Collaborate", + "collaboration": "Collaboration", + "collaborationWorkspace": "Collaboration Workspace", + "connected": "Connected", + "connectedSession": "Connected to a collaboration session", + "copiedInvitation": "Invitation code copied to clipboard.", + "copyAgain": "Copy Again", + "createRoom": "Create New Collaboration Session", + "creatingRoom": "Creating Session", + "end": "End Collaboration Session", + "endDetail": "Terminate the session, cease content sharing, and revoke access for others.", + "enterCode": "Enter collaboration session code", + "failedCreate": "Failed to create room: {0}", + "failedJoin": "Failed to join room: {0}", + "invite": "Invite Others", + "inviteDetail": "Copy the invitation code for sharing it with others to join the session.", + "joinRoom": "Join Collaboration Session", + "joiningRoom": "Joining Session", + "leave": "Leave Collaboration Session", + "leaveDetail": "Disconnect from the current collaboration session and close the workspace.", + "selectCollaboration": "Select collaboration option", + "sharedSession": "Shared a collaboration session", + "startSession": "Start or join collaboration session", + "userWantsToJoin": "User '{0}' wants to join the collaboration room", + "whatToDo": "What would you like to do with other collaborators?" + }, "core": { "about": { "compatibility": "{0} Compatibility", @@ -65,6 +98,13 @@ "next": "Next (Down)", "previous": "Previous (Up)" }, + "secondaryWindow": { + "alwaysOnTop": "When enabled, the secondary window stays above all other windows, including those of different applications.", + "description": "Sets the initial position and size of the extracted secondary window.", + "fullSize": "The position and size of the extracted widget will be the same as the running Theia application.", + "halfWidth": "The position and size of the extracted widget will be half the width of the running Theia application.", + "originalSize": "The position and size of the extracted widget will be the same as the original widget." + }, "silentNotifications": "Controls whether to suppress notification popups.", "tabDefaultSize": "Specifies the default size for tabs.", "tabMaximize": "Controls whether to maximize tabs on double click.", @@ -94,14 +134,32 @@ "toggleTracing": "Enable/disable tracing communications with debug adapters" }, "editor": { + "diffEditor.wordWrap2": "Lines will wrap according to the `#editor.wordWrap#` setting.", "dirtyEncoding": "The file is dirty. Please save it first before reopening it with another encoding.", - "editor.codeActionWidget.showHeaders": "Enable/disable showing group headers in the code action menu.", - "editor.experimental.pasteActions.enabled": "Enable/disable running edits from extensions on paste.", + "editor.accessibilitySupport0": "Use platform APIs to detect when a Screen Reader is attached", + "editor.accessibilitySupport1": "Optimize for usage with a Screen Reader", + "editor.accessibilitySupport2": "Assume a screen reader is not attached", + "editor.bracketPairColorization.enabled": "Controls whether bracket pair colorization is enabled or not. Use `#workbench.colorCustomizations#` to override the bracket highlight colors.", + "editor.codeActionWidget.includeNearbyQuickfixes": "Enable/disable showing nearest quickfix within a line when not currently on a diagnostic.", + "editor.cursorSurroundingLinesStyle": "Controls when `#cursorSurroundingLines#` should be enforced.", + "editor.detectIndentation": "Controls whether `#editor.tabSize#` and `#editor.insertSpaces#` will be automatically detected when a file is opened based on the file contents.", + "editor.dropIntoEditor.enabled": "Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor).", "editor.formatOnSaveMode.modificationsIfAvailable": "Will attempt to format modifications only (requires source control). If source control can't be used, then the whole file will be formatted.", + "editor.hover.hidingDelay": "Controls the delay in milliseconds after thich the hover is hidden. Requires `editor.hover.sticky` to be enabled.", "editor.inlayHints.enabled1": "Inlay hints are showing by default and hide when holding Ctrl+Alt", "editor.inlayHints.enabled2": "Inlay hints are hidden by default and show when holding Ctrl+Alt", + "editor.inlayHints.fontFamily": "Controls font family of inlay hints in the editor. When set to empty, the `#editor.fontFamily#` is used.", + "editor.inlayHints.fontSize": "Controls font size of inlay hints in the editor. As default the `#editor.fontSize#` is used when the configured value is less than `5` or greater than the editor font size.", + "editor.insertSpaces": "Insert spaces when pressing `Tab`. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on.", + "editor.occurrencesHighlight": "Controls whether the editor should highlight semantic symbol occurrences.", "editor.quickSuggestions": "Controls whether suggestions should automatically show up while typing. This can be controlled for typing in comments, strings, and other code. Quick suggestion can be configured to show as ghost text or with the suggest widget. Also be aware of the '#editor.suggestOnTriggerCharacters#'-setting which controls if suggestions are triggered by special characters.", - "editor.suggest.matchOnWordStartOnly": "When enabled IntelliSense filtering requires that the first character matches on a word start, e.g `c` on `Console` or `WebContext` but _not_ on `description`. When disabled IntelliSense will show more results but still sorts them by match quality.", + "editor.stickyScroll.scrollWithEditor": "Enable scrolling of the sticky scroll widget with the editor's horizontal scrollbar.", + "editor.suggestFontSize": "Font size for the suggest widget. When set to `0`, the value of `#editor.fontSize#` is used.", + "editor.suggestLineHeight": "Line height for the suggest widget. When set to `0`, the value of `#editor.lineHeight#` is used. The minimum value is 8.", + "editor.tabSize": "The number of spaces a tab is equal to. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on.", + "editor.useTabStops": "Inserting and deleting whitespace follows tab stops.", + "editor.wordBasedSuggestions": "Controls whether completions should be computed based on words in the document.", + "editor.wordBasedSuggestionsMode": "Controls from which documents word based completions are computed.", "files.autoSave": "Controls [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors that have unsaved changes.", "files.autoSave.afterDelay": "An editor with changes is automatically saved after the configured `#files.autoSaveDelay#`.", "files.autoSave.off": "An editor with changes is never automatically saved.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "a few seconds ago", "addSignedOff": "Add Signed-off-by", - "amendReuseMessag": "To reuse the last commit message, press 'Enter' or 'Escape' to cancel.", + "amendReuseMessage": "To reuse the last commit message, press 'Enter' or 'Escape' to cancel.", "amendRewrite": "Rewrite previous commit message. Press 'Enter' to confirm or 'Escape' to cancel.", "checkoutCreateLocalBranchWithName": "Create a new local branch with name: {0}. Press 'Enter' to confirm or 'Escape' to cancel.", "checkoutProvideBranchName": "Please provide a branch name. ", @@ -333,6 +391,9 @@ "alreadyRunning": "Hosted instance is already running.", "debugInstance": "Debug Instance", "debugMode": "Using inspect or inspect-brk for Node.js debug", + "debugPorts": { + "debugPort": "Port to use for this server's Node.js debug" + }, "devHost": "Development Host", "failed": "Failed to run hosted plugin instance: {0}", "hostedPlugin": "Hosted Plugin", @@ -364,6 +425,10 @@ "webviewTrace": "Controls communication tracing with webviews.", "webviewWarnIfUnsecure": "Warns users that webviews are currently deployed unsecurely." }, + "preferences": { + "hostedPlugin": "Hosted Plugin", + "toolbar": "Toolbar" + }, "preview": { "openByDefault": "Open the preview instead of the editor by default." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "hidden", "config.untrackedChanges.mixed": "mixed", "config.untrackedChanges.separate": "separate", + "dirtyDiff": { + "close": "Close Change Peek View" + }, "history": "History", "noRepositoryFound": "No repository found", "unamend": "Unamend", @@ -401,8 +469,7 @@ "extract-widget": "Move View to Secondary Window" }, "shell-area": { - "secondary": "Secondary Window", - "top": "Top" + "secondary": "Secondary Window" }, "task": { "attachTask": "Attach Task...", @@ -422,6 +489,7 @@ "profilePath": "The path of the shell that this profile uses.", "profiles": "The profiles to present when creating a new terminal. Set the path property manually with optional args.\nSet an existing profile to `null` to hide the profile from the list, for example: `\"{0}\": null`.", "rendererType": "Controls how the terminal is rendered.", + "rendererTypeDeprecationMessage": "The renderer type is no longer supported as an option.", "selectProfile": "Select a profile for the new terminal", "shell.deprecated": "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in 'terminal.integrated.profiles.{0}' and setting its profile name as the default in 'terminal.integrated.defaultProfile.{0}.'", "shellArgsLinux": "The command line arguments to use when on the Linux terminal.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Cancel All Test Runs", + "stackFrameAt": "at", "testRunDefaultName": "{0} run {1}", "testRuns": "Test Runs" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Supertype Hierarchy" }, "vsx-registry": { + "confirmDialogMessage": "The extension \"{0}\" is unverified and might pose a security risk.", + "confirmDialogTitle": "Are you sure you want to proceed with the installation ?", "downloadCount": "Download count: {0}", "errorFetching": "Error fetching extensions.", "errorFetchingConfigurationHint": "This could be caused by network configuration issues.", "failedInstallingVSIX": "Failed to install {0} from VSIX.", "invalidVSIX": "The selected file is not a valid \"*.vsix\" plugin.", "license": "License: {0}", + "onlyShowVerifiedExtensionsDescription": "This allows the {0} to only show verified extensions.", + "onlyShowVerifiedExtensionsTitle": "Only Show Verified Extensions", "recommendedExtensions": "Do you want to install the recommended extensions for this repository?", "searchPlaceholder": "Search Extensions in {0}", "showInstalled": "Show Installed Extensions", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "Do you really want to delete all the {0} selected files?", "confirmMessage.uriSingle": "Do you really want to delete {0}?", "duplicate": "Duplicate", - "failApply": "Could not apply changes to new file", "failSaveAs": "Cannot run \"{0}\" for the current widget.", "newFilePlaceholder": "File Name", "newFolderPlaceholder": "Folder Name", diff --git a/packages/core/i18n/nls.ko.json b/packages/core/i18n/nls.ko.json new file mode 100644 index 0000000000000..38710bd357012 --- /dev/null +++ b/packages/core/i18n/nls.ko.json @@ -0,0 +1,585 @@ +{ + "aiConfiguration:open": "AI 구성 보기 열기", + "aiHistory:open": "AI 기록 보기 열기", + "debug.breakpoint.editCondition": "조건 편집...", + "notebook.cell.changeToCode": "셀을 코드로 변경", + "notebook.cell.changeToMarkdown": "셀을 마크다운으로 변경", + "notebook.cell.insertMarkdownCellAbove": "위에 마크다운 셀 삽입", + "notebook.cell.insertMarkdownCellBelow": "아래에 마크다운 셀 삽입", + "terminal:new:profile": "프로필에서 새 통합 터미널 만들기", + "terminal:profile:default": "기본 터미널 프로필을 선택합니다.", + "theia": { + "callhierarchy": { + "noCallers": "발신자가 감지되지 않았습니다.", + "open": "오픈 콜 계층 구조" + }, + "collaboration": { + "collaborate": "협업", + "collaboration": "협업", + "collaborationWorkspace": "협업 작업 공간", + "connected": "연결됨", + "connectedSession": "공동 작업 세션에 연결됨", + "copiedInvitation": "초대 코드가 클립보드에 복사되었습니다.", + "copyAgain": "다시 복사", + "createRoom": "새 공동 작업 세션 만들기", + "creatingRoom": "세션 만들기", + "end": "협업 세션 종료", + "endDetail": "세션을 종료하고, 콘텐츠 공유를 중단하고, 다른 사용자의 액세스 권한을 취소합니다.", + "enterCode": "공동 작업 세션 코드 입력", + "failedCreate": "공간을 만들지 못했습니다: {0}", + "failedJoin": "방에 참여하지 못했습니다: {0}", + "invite": "다른 사람 초대", + "inviteDetail": "초대 코드를 복사하여 다른 사람들과 공유하여 세션에 참여합니다.", + "joinRoom": "협업 세션 참여", + "joiningRoom": "세션 참여", + "leave": "공동 작업 세션 나가기", + "leaveDetail": "현재 공동 작업 세션에서 연결을 끊고 워크스페이스를 닫습니다.", + "selectCollaboration": "협업 옵션 선택", + "sharedSession": "공동 작업 세션 공유", + "startSession": "공동 작업 세션 시작 또는 참여", + "userWantsToJoin": "사용자 '{0}' 가 공동 작업실에 참여하려고 합니다.", + "whatToDo": "다른 공동 작업자들과 함께 무엇을 하고 싶으신가요?" + }, + "core": { + "about": { + "compatibility": "{0} 호환성", + "defaultApi": "기본값 {0} API", + "version": "버전" + }, + "common": { + "closeAll": "모든 탭 닫기", + "closeAllTabMain": "메인 영역의 모든 탭 닫기", + "closeOtherTabMain": "메인 영역에서 다른 탭 닫기", + "closeOthers": "다른 탭 닫기", + "closeRight": "오른쪽 탭 닫기", + "closeTab": "탭 닫기", + "closeTabMain": "메인 영역에서 탭 닫기", + "collapseAllTabs": "모든 측면 패널 접기", + "collapseBottomPanel": "하단 패널 토글", + "collapseTab": "측면 패널 접기", + "showNextTabGroup": "다음 탭 그룹으로 전환", + "showNextTabInGroup": "그룹에서 다음 탭으로 전환", + "showPreviousTabGroup": "이전 탭 그룹으로 전환", + "showPreviousTabInGroup": "그룹에서 이전 탭으로 전환", + "toggleMaximized": "최대화 토글" + }, + "copyInfo": "먼저 파일을 열어 경로를 복사합니다.", + "copyWarn": "브라우저의 복사 명령 또는 바로 가기를 사용하세요.", + "cutWarn": "브라우저의 잘라내기 명령 또는 바로 가기를 사용하세요.", + "enhancedPreview": { + "classic": "기본 정보가 포함된 탭의 간단한 미리 보기를 표시합니다.", + "enhanced": "추가 정보가 포함된 탭의 향상된 미리 보기를 표시합니다.", + "visual": "탭의 시각적 미리 보기를 표시합니다." + }, + "file": { + "browse": "찾아보기" + }, + "highlightModifiedTabs": "수정된(더티) 편집기 탭에 상단 테두리를 그릴지 여부를 제어합니다.", + "keybindingStatus": "{0} 키를 누르고 더 많은 키를 기다렸습니다.", + "keyboard": { + "choose": "키보드 레이아웃 선택", + "chooseLayout": "키보드 레이아웃 선택", + "current": "(현재: {0})", + "currentLayout": " - 현재 레이아웃", + "mac": "Mac 키보드", + "pc": "PC 키보드", + "tryDetect": "브라우저 정보 및 누른 키에서 키보드 레이아웃을 감지해 보세요." + }, + "navigator": { + "clipboardWarn": "클립보드에 대한 액세스가 거부되었습니다. 브라우저의 권한을 확인하세요.", + "clipboardWarnFirefox": "클립보드 API를 사용할 수 없습니다. '{0}' 페이지의 '{1}' 환경 설정에서 활성화할 수 있습니다. 그런 다음 Theia를 다시 로드합니다. 이렇게 하면 FireFox가 시스템 클립보드에 대한 전체 액세스 권한을 갖게 됩니다." + }, + "offline": "오프라인", + "pasteWarn": "브라우저의 붙여넣기 명령 또는 바로 가기를 사용하세요.", + "quitMessage": "저장하지 않은 변경 사항은 저장되지 않습니다.", + "resetWorkbenchLayout": "워크벤치 레이아웃 재설정", + "searchbox": { + "close": "닫기(탈출)", + "next": "다음 (아래로)", + "previous": "이전 (위로)" + }, + "secondaryWindow": { + "alwaysOnTop": "활성화하면 보조 창이 다른 애플리케이션의 창을 포함한 다른 모든 창 위에 표시됩니다.", + "description": "추출된 보조 창의 초기 위치와 크기를 설정합니다.", + "fullSize": "추출된 위젯의 위치와 크기는 실행 중인 테아 애플리케이션과 동일합니다.", + "halfWidth": "추출된 위젯의 위치와 크기는 실행 중인 테아 애플리케이션 너비의 절반이 됩니다.", + "originalSize": "추출된 위젯의 위치와 크기는 원래 위젯과 동일합니다." + }, + "silentNotifications": "알림 팝업 표시 여부를 제어합니다.", + "tabDefaultSize": "탭의 기본 크기를 지정합니다.", + "tabMaximize": "더블 클릭 시 탭을 최대화할지 여부를 제어합니다.", + "tabMinimumSize": "탭의 최소 크기를 지정합니다.", + "tabShrinkToFit": "사용 가능한 공간에 맞게 탭을 축소합니다." + }, + "debug": { + "addConfigurationPlaceholder": "구성을 추가할 작업 공간 루트를 선택합니다.", + "breakpoint": "브레이크포인트", + "compound-cycle": "실행 구성 '{0}' 자체에 사이클이 포함되어 있습니다.", + "continueAll": "모두 계속하기", + "copyExpressionValue": "표현식 값 복사", + "dataBreakpoint": "데이터 중단점", + "debugVariableInput": "{0} 값 설정", + "entry": "항목", + "exception": "예외", + "functionBreakpoint": "함수 중단점", + "goto": "goto", + "instruction-breakpoint": "명령어 중단점", + "instructionBreakpoint": "명령어 중단점", + "missingConfiguration": "동적 구성 '{0}:{1}' 이 누락되었거나 해당되지 않습니다.", + "pause": "일시 중지", + "pauseAll": "모두 일시 중지", + "reveal": "공개", + "step": "단계", + "threads": "스레드", + "toggleTracing": "디버그 어댑터와의 추적 통신 활성화/비활성화" + }, + "editor": { + "diffEditor.wordWrap2": "줄 바꿈은 `#편집기.wordWrap#` 설정에 따라 줄 바꿈됩니다.", + "dirtyEncoding": "파일이 더럽습니다. 다른 인코딩으로 다시 열기 전에 먼저 저장해 주세요.", + "editor.accessibilitySupport0": "플랫폼 API를 사용하여 스크린 리더가 연결된 경우 감지하기", + "editor.accessibilitySupport1": "화면 리더 사용에 맞게 최적화", + "editor.accessibilitySupport2": "화면 리더가 연결되어 있지 않다고 가정합니다.", + "editor.bracketPairColorization.enabled": "대괄호 쌍 색상화 사용 여부를 제어합니다. 대괄호 하이라이트 색상을 재정의하려면 `#workbench.colorCustomizations#`를 사용합니다.", + "editor.codeActionWidget.includeNearbyQuickfixes": "현재 진단 중이 아닐 때 줄 내에서 가장 가까운 퀵픽스 표시를 사용/사용 안 함으로 설정합니다.", + "editor.cursorSurroundingLinesStyle": "커서 서라운드 라인 적용 시기를 제어합니다.", + "editor.detectIndentation": "파일 내용을 기반으로 파일을 열 때 `#편집기.탭 크기#` 및 `#편집기.삽입 공백#`을 자동으로 감지할지 여부를 제어합니다.", + "editor.dropIntoEditor.enabled": "편집기에서 파일을 여는 대신 'Shift' 키를 누른 채로 파일을 텍스트 편집기로 끌어다 놓을 수 있는지 여부를 제어합니다.", + "editor.formatOnSaveMode.modificationsIfAvailable": "수정 사항만 포맷하려고 시도합니다(소스 제어 필요). 소스 제어를 사용할 수 없는 경우 전체 파일이 포맷됩니다.", + "editor.hover.hidingDelay": "호버가 숨겨지는 지연 시간(밀리초)을 제어합니다. editor.hover.sticky`가 활성화되어 있어야 합니다.", + "editor.inlayHints.enabled1": "인레이 힌트는 기본적으로 표시되며 Ctrl+Alt를 누르고 있으면 숨겨집니다.", + "editor.inlayHints.enabled2": "인레이 힌트는 기본적으로 숨겨져 있으며 Ctrl+Alt를 누르고 있으면 표시됩니다.", + "editor.inlayHints.fontFamily": "편집기에서 인레이 힌트의 글꼴 패밀리를 제어합니다. 비워두면 `#editor.fontFamily#`가 사용됩니다.", + "editor.inlayHints.fontSize": "에디터에서 인레이 힌트의 글꼴 크기를 제어합니다. 기본적으로 설정된 값이 `5`보다 작거나 편집기 글꼴 크기보다 큰 경우 `#editor.fontSize#`가 사용됩니다.", + "editor.insertSpaces": "Tab`을 누를 때 공백을 삽입합니다. 이 설정은 `#편집기 감지 들여쓰기#`가 켜져 있을 때 파일 내용에 따라 재정의됩니다.", + "editor.occurrencesHighlight": "편집기에서 의미 기호 발생을 강조 표시할지 여부를 제어합니다.", + "editor.quickSuggestions": "입력하는 동안 제안을 자동으로 표시할지 여부를 제어합니다. 주석, 문자열 및 기타 코드를 입력할 때 이 기능을 제어할 수 있습니다. 빠른 제안은 고스트 텍스트로 표시하거나 제안 위젯과 함께 표시하도록 구성할 수 있습니다. 또한 특수 문자에 의해 제안이 트리거되는지 여부를 제어하는 '#editor.suggestOnTriggerCharacters#'설정도 알아두세요.", + "editor.stickyScroll.scrollWithEditor": "편집기의 가로 스크롤 막대를 사용하여 고정 스크롤 위젯의 스크롤을 활성화합니다.", + "editor.suggestFontSize": "추천 위젯의 글꼴 크기입니다. 0`으로 설정하면 `#editor.fontSize#`의 값이 사용됩니다.", + "editor.suggestLineHeight": "제안 위젯의 줄 높이입니다. '0'으로 설정하면 '#편집기줄높이#'의 값이 사용됩니다. 최소값은 8입니다.", + "editor.tabSize": "탭의 공백 수입니다. 이 설정은 `#편집기 감지 들여쓰기#`가 켜져 있을 때 파일 내용에 따라 재정의됩니다.", + "editor.useTabStops": "공백을 삽입하고 삭제하면 탭이 멈춥니다.", + "editor.wordBasedSuggestions": "문서의 단어를 기준으로 완성을 계산할지 여부를 제어합니다.", + "editor.wordBasedSuggestionsMode": "문서에서 단어 기반 완성을 계산하는 컨트롤입니다.", + "files.autoSave": "저장되지 않은 변경 사항이 있는 편집기의 [자동 저장](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save)을 제어합니다.", + "files.autoSave.afterDelay": "변경 사항이 있는 편집기는 설정된 `#files.autoSaveDelay#`가 지난 후에 자동으로 저장됩니다.", + "files.autoSave.off": "변경 사항이 있는 편집기는 자동으로 저장되지 않습니다.", + "files.autoSave.onFocusChange": "편집기가 포커스를 잃으면 변경 사항이 있는 편집기가 자동으로 저장됩니다.", + "files.autoSave.onWindowChange": "창이 포커스를 잃으면 변경 사항이 있는 편집기가 자동으로 저장됩니다.", + "formatOnSaveTimeout": "파일 저장 시 실행되는 서식이 취소되는 시간 초과(밀리초)입니다.", + "persistClosedEditors": "창을 다시 로드할 때 작업 영역의 닫힌 편집기 기록을 유지할지 여부를 제어합니다.", + "showAllEditors": "열려 있는 모든 편집기 표시", + "splitHorizontal": "편집기 가로 분할", + "splitVertical": "편집기 세로 분할", + "toggleStickyScroll": "고정 스크롤 토글" + }, + "file-search": { + "toggleIgnoredFiles": " (무시된 파일을 표시/숨기려면 {0} 을 누르세요.)" + }, + "fileDialog": { + "showHidden": "숨겨진 파일 표시" + }, + "fileSystem": { + "fileResource": { + "overWriteBody": "파일 시스템의 '{0}' 변경 내용을 덮어쓰시겠습니까?" + } + }, + "filesystem": { + "copiedToClipboard": "다운로드 링크를 클립보드에 복사합니다.", + "copyDownloadLink": "다운로드 링크 복사", + "dialog": { + "initialLocation": "초기 위치로 이동", + "multipleItemMessage": "하나의 항목만 선택할 수 있습니다.", + "name": "이름:", + "navigateBack": "뒤로 이동", + "navigateForward": "앞으로 탐색", + "navigateUp": "하나의 디렉터리로 이동" + }, + "fileResource": { + "binaryFileQuery": "열면 시간이 걸리고 IDE가 응답하지 않을 수 있습니다. 그래도 '{0}' 을 열시겠습니까?", + "binaryTitle": "파일이 바이너리이거나 지원되지 않는 텍스트 인코딩을 사용합니다.", + "largeFileTitle": "파일이 너무 큽니다({0}).", + "overwriteTitle": "파일 시스템에서 '{0}' 파일이 변경되었습니다." + }, + "filesExclude": "파일 및 폴더를 제외하기 위한 글로브 패턴을 구성합니다. 예를 들어, 파일 탐색기는 이 설정에 따라 표시하거나 숨길 파일 및 폴더를 결정합니다.", + "format": "형식:", + "maxConcurrentUploads": "여러 파일을 업로드할 때 업로드할 수 있는 최대 동시 파일 수입니다. 0은 모든 파일이 동시에 업로드됨을 의미합니다.", + "maxFileSizeMB": "열 수 있는 최대 파일 크기(MB)를 제어합니다.", + "prepareDownload": "다운로드 준비 중...", + "prepareDownloadLink": "다운로드 링크 준비 중...", + "processedOutOf": "처리된 {0} 중 {1}", + "replaceTitle": "파일 바꾸기", + "uploadFiles": "파일 업로드...", + "uploadedOutOf": "업로드 {0} 중 {1}" + }, + "getting-started": { + "apiComparator": "{0} API 호환성", + "newExtension": "새 확장 프로그램 구축", + "newPlugin": "새 플러그인 구축", + "startup-editor": { + "welcomePage": "{0} 및 확장 프로그램을 시작하는 데 도움이 되는 콘텐츠가 있는 시작 페이지를 엽니다." + } + }, + "git": { + "aFewSecondsAgo": "몇 초 전", + "addSignedOff": "결재자 추가", + "amendReuseMessage": "마지막 커밋 메시지를 다시 사용하려면 'Enter' 또는 'Escape'를 눌러 취소합니다.", + "amendRewrite": "이전 커밋 메시지를 다시 작성합니다. 'Enter'를 눌러 확인하거나 'Escape'를 눌러 취소합니다.", + "checkoutCreateLocalBranchWithName": "{0} 이라는 이름으로 새 로컬 브랜치를 만듭니다. 'Enter'를 눌러 확인하거나 'Escape'를 눌러 취소합니다.", + "checkoutProvideBranchName": "지점 이름을 입력하세요. ", + "checkoutSelectRef": "결제할 참조를 선택하거나 새 로컬 지점을 생성합니다:", + "cloneQuickInputLabel": "Git 리포지토리 위치를 입력하세요. 'Enter'를 눌러 확인하거나 'Escape'를 눌러 취소합니다.", + "cloneRepository": "Git 리포지토리를 복제합니다: {0}. 'Enter'를 눌러 확인하거나 'Escape'를 눌러 취소합니다.", + "compareWith": "비교 대상...", + "compareWithBranchOrTag": "현재 활성화된 {0} 브랜치와 비교할 브랜치 또는 태그를 선택합니다:", + "diff": "Diff", + "dirtyDiffLinesLimit": "편집기의 줄 수가 이 제한을 초과하는 경우 더티 디프 장식을 표시하지 않습니다.", + "dropStashMessage": "보관함이 성공적으로 제거되었습니다.", + "editorDecorationsEnabled": "에디터에서 git 장식을 표시합니다.", + "fetchPickRemote": "가져올 리모컨을 선택합니다:", + "gitDecorationsColors": "내비게이터에서 색상 장식을 사용합니다.", + "mergeQuickPickPlaceholder": "현재 활성 상태인 {0} 브랜치에 병합할 브랜치를 선택합니다:", + "missingUserInfo": "git에서 'user.name'과 'user.email'을 구성했는지 확인하세요.", + "noHistoryForError": "다음에 사용할 수 있는 기록이 없습니다. {0}", + "noPreviousCommit": "수정할 이전 커밋이 없습니다.", + "noRepositoriesSelected": "선택한 리포지토리가 없습니다.", + "prepositionIn": "in", + "repositoryNotInitialized": "리포지토리 {0} 가 아직 초기화되지 않았습니다.", + "stashChanges": "변경 사항을 저장합니다. 'Enter'를 눌러 확인하거나 'Escape'를 눌러 취소합니다.", + "stashChangesWithMessage": "메시지와 함께 변경 사항을 저장합니다: {0}. 'Enter'를 눌러 확인하거나 'Escape'를 눌러 취소합니다.", + "tabTitleIndex": "{0} (색인)", + "tabTitleWorkingTree": "{0} (작업 트리)", + "toggleBlameAnnotations": "비난 주석 토글" + }, + "keybinding-schema-updater": { + "deprecation": "대신 '언제' 절을 사용합니다." + }, + "keymaps": { + "addKeybindingTitle": "다음에 대한 키 바인딩 추가 {0}", + "editKeybinding": "키 바인딩 편집...", + "editKeybindingTitle": "다음에 대한 키 바인딩 편집 {0}", + "editWhenExpression": "표현식 편집...", + "editWhenExpressionTitle": "다음에 대한 표현식 편집 {0}", + "keybinding": { + "copy": "키 바인딩 복사", + "copyCommandId": "키 바인딩 명령 ID 복사", + "copyCommandTitle": "키 바인딩 명령 제목 복사", + "edit": "키 바인딩 편집...", + "editWhenExpression": "표현식 편집 시 키 바인딩..." + }, + "keybindingCollidesValidation": "현재 충돌하는 키 바인딩", + "requiredKeybindingValidation": "키 바인딩 값은 필수입니다.", + "resetKeybindingConfirmation": "이 키 바인딩을 정말 기본값으로 재설정하시겠습니까?", + "resetKeybindingTitle": "다음에 대한 키 바인딩 재설정 {0}", + "resetMultipleKeybindingsWarning": "이 명령에 대해 여러 개의 키 바인딩이 있는 경우 모두 재설정됩니다." + }, + "localize": { + "offlineTooltip": "백엔드에 연결할 수 없습니다." + }, + "markers": { + "clearAll": "모두 지우기", + "noProblems": "지금까지 워크스페이스에서 문제가 감지되지 않았습니다.", + "tabbarDecorationsEnabled": "탭 표시줄에 문제 데코레이터(진단 마커)를 표시합니다." + }, + "memory-inspector": { + "addressTooltip": "표시할 메모리 위치, 주소 또는 주소로 평가하는 표현식", + "ascii": "ASCII", + "binary": "바이너리", + "byteSize": "바이트 크기", + "bytesPerGroup": "그룹당 바이트 수", + "closeSettings": "설정 닫기", + "columns": "열", + "command": { + "createNewMemory": "새 메모리 검사기 만들기", + "createNewRegisterView": "새 등록 보기 만들기", + "followPointer": "포인터 팔로우", + "followPointerMemory": "메모리 인스펙터에서 포인터 팔로우", + "resetValue": "값 재설정", + "showRegister": "메모리 검사기에서 등록 표시", + "viewVariable": "메모리 인스펙터에서 변수 표시" + }, + "data": "데이터", + "decimal": "십진수", + "diff": { + "label": "Diff: {0}" + }, + "diff-widget": { + "offset-label": "{0} 오프셋", + "offset-title": "메모리에서 오프셋할 바이트 {0}" + }, + "editable": { + "apply": "변경 사항 적용", + "clear": "변경 사항 지우기" + }, + "endianness": "엔디안", + "extraColumn": "추가 열", + "groupsPerRow": "행별 그룹", + "hexadecimal": "16진수", + "length": "길이", + "lengthTooltip": "가져올 바이트 수(10진수 또는 16진수)", + "memory": { + "addressField": { + "memoryReadError": "위치 필드에 주소 또는 표현식을 입력합니다." + }, + "freeze": "메모리 고정 보기", + "hideSettings": "설정 패널 숨기기", + "readError": { + "bounds": "메모리 한도를 초과하면 결과가 잘립니다.", + "noContents": "현재 사용할 수 있는 메모리 콘텐츠가 없습니다." + }, + "readLength": { + "memoryReadError": "길이 필드에 길이(10진수 또는 16진수)를 입력합니다." + }, + "showSettings": "설정 패널 표시", + "unfreeze": "메모리 고정 해제 보기", + "userError": "메모리를 가져오는 동안 오류가 발생했습니다." + }, + "memoryCategory": "메모리 검사기", + "memoryInspector": "메모리 검사기", + "memoryTitle": "메모리", + "octal": "옥탈", + "offset": "오프셋", + "offsetTooltip": "탐색 시 현재 메모리 위치에 추가할 오프셋입니다.", + "provider": { + "localsError": "로컬 변수를 읽을 수 없습니다. 활성 디버그 세션이 없습니다.", + "readError": "메모리를 읽을 수 없습니다. 활성 디버그 세션이 없습니다.", + "writeError": "메모리를 쓸 수 없습니다. 활성 디버그 세션이 없습니다." + }, + "register": "등록하기", + "register-widget": { + "filter-placeholder": "필터(로 시작)" + }, + "registerReadError": "레지스터를 가져오는 동안 오류가 발생했습니다.", + "registers": "레지스터", + "toggleComparisonWidgetVisibility": "비교 위젯 표시 여부 토글", + "utils": { + "afterBytes": "비교하려는 두 위젯 모두에 메모리를 로드해야 합니다. {0} 메모리가 로드되지 않았습니다.", + "bytesMessage": "비교하려는 두 위젯 모두에 메모리를 로드해야 합니다. {0} 메모리가 로드되지 않았습니다." + } + }, + "messages": { + "notificationTimeout": "이 시간 초과 후에는 정보 알림이 숨겨집니다.", + "toggleNotifications": "알림 토글" + }, + "mini-browser": { + "typeUrl": "URL 입력" + }, + "monaco": { + "noSymbolsMatching": "일치하는 기호 없음", + "typeToSearchForSymbols": "기호를 검색하려면 입력하세요." + }, + "navigator": { + "autoReveal": "자동 공개", + "clipboardWarn": "클립보드에 대한 액세스가 거부되었습니다. 브라우저의 권한을 확인하세요.", + "clipboardWarnFirefox": "클립보드 API를 사용할 수 없습니다. '{0}' 페이지의 '{1}' 환경 설정에서 활성화할 수 있습니다. 그런 다음 Theia를 다시 로드합니다. 이렇게 하면 FireFox가 시스템 클립보드에 대한 전체 액세스 권한을 갖게 됩니다.", + "refresh": "탐색기에서 새로 고침", + "reveal": "탐색기에서 공개", + "toggleHiddenFiles": "숨겨진 파일 토글" + }, + "output": { + "clearOutputChannel": "출력 채널 지우기...", + "closeOutputChannel": "출력 채널 닫기...", + "hiddenChannels": "숨겨진 채널", + "hideOutputChannel": "출력 채널 숨기기...", + "maxChannelHistory": "출력 채널의 최대 항목 수입니다.", + "outputChannels": "출력 채널", + "showOutputChannel": "출력 채널 표시..." + }, + "plugin": { + "blockNewTab": "브라우저가 새 탭을 열지 못했습니다." + }, + "plugin-dev": { + "alreadyRunning": "호스팅된 인스턴스가 이미 실행 중입니다.", + "debugInstance": "인스턴스 디버그", + "debugMode": "Node.js 디버그에 inspect 또는 inspect-brk 사용", + "debugPorts": { + "debugPort": "이 서버의 Node.js 디버그에 사용할 포트" + }, + "devHost": "개발 호스트", + "failed": "호스팅된 플러그인 인스턴스를 실행하지 못했습니다: {0}", + "hostedPlugin": "호스팅 플러그인", + "hostedPluginRunning": "호스팅 플러그인: 실행 중", + "hostedPluginStarting": "호스팅 플러그인: 시작", + "hostedPluginStopped": "호스팅 플러그인: 중지됨", + "hostedPluginWatching": "호스팅 플러그인: 보기", + "instanceTerminated": "{0} 종료되었습니다.", + "launchOutFiles": "생성된 자바스크립트 파일을 찾기 위한 글로브 패턴 배열(`${플러그인패스}`는 플러그인 실제 경로로 대체됨).", + "noValidPlugin": "지정한 폴더에 유효한 플러그인이 포함되어 있지 않습니다.", + "notRunning": "호스팅된 인스턴스가 실행되고 있지 않습니다.", + "pluginFolder": "플러그인 폴더로 설정됩니다: {0}", + "preventedNewTab": "브라우저가 새 탭을 열지 못했습니다.", + "restartInstance": "인스턴스 다시 시작", + "running": "호스팅된 인스턴스가 실행 중입니다:", + "select": "선택", + "selectPath": "경로 선택", + "startInstance": "인스턴스 시작", + "starting": "호스팅 인스턴스 서버 시작 ...", + "stopInstance": "인스턴스 중지", + "unknownTerminated": "인스턴스가 종료되었습니다.", + "watchMode": "개발 중인 플러그인에서 감시자 실행" + }, + "plugin-ext": { + "authentication-main": { + "loginTitle": "로그인" + }, + "plugins": "플러그인", + "webviewTrace": "웹뷰로 통신 추적을 제어합니다.", + "webviewWarnIfUnsecure": "웹뷰가 현재 안전하지 않게 배포되었음을 사용자에게 경고합니다." + }, + "preferences": { + "hostedPlugin": "호스팅 플러그인", + "toolbar": "도구 모음" + }, + "preview": { + "openByDefault": "기본적으로 편집기 대신 미리 보기를 엽니다." + }, + "property-view": { + "created": "생성됨", + "directory": "디렉토리", + "lastModified": "마지막 수정", + "location": "위치", + "noProperties": "사용 가능한 숙소가 없습니다.", + "properties": "속성", + "size": "크기", + "symbolicLink": "기호 링크" + }, + "scm": { + "amend": "수정", + "amendHeadCommit": "HEAD 커밋", + "amendLastCommit": "마지막 커밋 수정", + "changeRepository": "리포지토리 변경...", + "config.untrackedChanges": "추적되지 않은 변경 사항의 작동 방식을 제어합니다.", + "config.untrackedChanges.hidden": "숨겨진", + "config.untrackedChanges.mixed": "혼합", + "config.untrackedChanges.separate": "분리", + "dirtyDiff": { + "close": "변경 사항 미리 보기 닫기" + }, + "history": "역사", + "noRepositoryFound": "리포지토리를 찾을 수 없습니다.", + "unamend": "수정 취소", + "unamendCommit": "커밋 수정 취소" + }, + "search-in-workspace": { + "includeIgnoredFiles": "무시된 파일 포함", + "noFolderSpecified": "폴더를 열거나 지정하지 않았습니다. 현재 열려 있는 파일만 검색됩니다.", + "resultSubset": "이는 전체 결과의 일부일 뿐입니다. 보다 구체적인 검색어를 사용하여 결과 목록의 범위를 좁히세요.", + "searchOnEditorModification": "수정한 경우 활성 편집기를 검색합니다." + }, + "secondary-window": { + "extract-widget": "보기를 보조 창으로 이동" + }, + "shell-area": { + "secondary": "보조 창" + }, + "task": { + "attachTask": "작업 첨부...", + "clearHistory": "기록 지우기", + "noTaskToRun": "실행할 작업을 찾을 수 없습니다. 작업 구성...", + "openUserTasks": "사용자 작업 열기" + }, + "terminal": { + "defaultProfile": "다음에서 사용되는 기본 프로필 {0}", + "enableCopy": "ctrl-c(macOS의 경우 cmd-c)를 활성화하여 선택한 텍스트를 복사합니다.", + "enablePaste": "클립보드에서 붙여넣기하려면 ctrl-v(macOS의 경우 cmd-v)를 사용하도록 설정합니다.", + "profileArgs": "이 프로필이 사용하는 셸 인수입니다.", + "profileColor": "단말기와 연결할 단말기 테마 색상 ID입니다.", + "profileDefault": "기본 프로필...을 선택합니다.", + "profileIcon": "터미널 아이콘과 연결할 코디콘 ID입니다.\nterminal-tmux:\"$(terminal-tmux)\"", + "profileNew": "새 터미널(프로필 포함)...", + "profilePath": "이 프로필이 사용하는 셸의 경로입니다.", + "profiles": "새 터미널을 만들 때 표시할 프로필입니다. 선택적 인수를 사용하여 경로 속성을 수동으로 설정합니다.\n기존 프로필을 `null`로 설정하면 목록에서 프로필을 숨길 수 있습니다(예: `\"{0}\": null`).", + "rendererType": "터미널 렌더링 방식을 제어합니다.", + "rendererTypeDeprecationMessage": "렌더러 유형은 더 이상 옵션으로 지원되지 않습니다.", + "selectProfile": "새 터미널의 프로필을 선택합니다.", + "shell.deprecated": "이 방법은 더 이상 사용되지 않으며, 기본 셸을 구성하는 새로운 권장 방법은 'terminal.integrated.profiles.{0}' 에서 터미널 프로필을 생성하고 'terminal.integrated.defaultProfile.{0}' 에서 프로필 이름을 기본값으로 설정하는 것입니다.", + "shellArgsLinux": "Linux 터미널에서 사용할 명령줄 인수입니다.", + "shellArgsOsx": "macOS 터미널에서 사용할 명령줄 인수입니다.", + "shellArgsWindows": "Windows 터미널에서 사용할 명령줄 인수입니다.", + "shellLinux": "터미널이 Linux에서 사용하는 셸 경로입니다(기본값: '{0}'}).", + "shellOsx": "터미널이 macOS에서 사용하는 셸의 경로(기본값: '{0}'})입니다.", + "shellWindows": "터미널이 Windows에서 사용하는 셸의 경로입니다. (기본값: '{0}')." + }, + "test": { + "cancelAllTestRuns": "모든 테스트 실행 취소", + "stackFrameAt": "에서", + "testRunDefaultName": "{0} 실행 {1}", + "testRuns": "테스트 실행" + }, + "toolbar": { + "addCommand": "도구 모음에 명령 추가", + "addCommandPlaceholder": "도구 모음에 추가할 명령 찾기", + "centerColumn": "중앙 열", + "failedUpdate": "'{1}'의 '{0}' 값을 업데이트하지 못했습니다.", + "filterIcons": "필터 아이콘", + "iconSelectDialog": "'{0}' 아이콘을 선택합니다.", + "iconSet": "아이콘 세트", + "insertGroupLeft": "그룹 구분 기호 삽입(왼쪽)", + "insertGroupRight": "그룹 구분 기호 삽입(오른쪽)", + "leftColumn": "왼쪽 열", + "openJSON": "툴바 사용자 지정(JSON 열기)", + "removeCommand": "도구 모음에서 명령 제거", + "restoreDefaults": "도구 모음 기본값 복원", + "rightColumn": "오른쪽 열", + "selectIcon": "아이콘 선택", + "toggleToolbar": "툴바 토글", + "toolbarLocationPlaceholder": "명령어를 어디에 추가하고 싶으신가요?", + "useDefaultIcon": "기본 아이콘 사용" + }, + "typehierarchy": { + "subtypeHierarchy": "하위 유형 계층 구조", + "supertypeHierarchy": "슈퍼타입 계층 구조" + }, + "vsx-registry": { + "confirmDialogMessage": "확장자 \"{0}\"는 확인되지 않았으며 보안 위험을 초래할 수 있습니다.", + "confirmDialogTitle": "설치를 계속 진행하시겠습니까?", + "downloadCount": "다운로드 횟수: {0}", + "errorFetching": "확장 프로그램 가져오기 오류가 발생했습니다.", + "errorFetchingConfigurationHint": "이는 네트워크 구성 문제로 인해 발생할 수 있습니다.", + "failedInstallingVSIX": "VSIX에서 {0} 설치에 실패했습니다.", + "invalidVSIX": "선택한 파일이 유효한 \"*.vsix\" 플러그인이 아닙니다.", + "license": "라이선스: {0}", + "onlyShowVerifiedExtensionsDescription": "이렇게 하면 {0} 에 확인된 확장자만 표시할 수 있습니다.", + "onlyShowVerifiedExtensionsTitle": "인증된 확장 프로그램만 표시", + "recommendedExtensions": "이 리포지토리에 권장되는 확장 프로그램을 설치하시겠습니까?", + "searchPlaceholder": "에서 확장 프로그램 검색 {0}", + "showInstalled": "설치된 확장 프로그램 표시", + "showRecommendedExtensions": "확장 프로그램 권장 사항에 대한 알림 표시 여부를 제어합니다.", + "vsx-extensions-contribution": { + "update-version-uninstall-error": "확장 프로그램을 제거하는 동안 오류가 발생했습니다: {0}.", + "update-version-version-error": "{1} 의 버전 {0} 을 설치하지 못했습니다." + } + }, + "webview": { + "goToReadme": "README로 이동", + "messageWarning": " {0} 엔드포인트의 호스트 패턴이 `{1}`로 변경되었으며, 패턴을 변경하면 보안 취약점이 발생할 수 있습니다. 자세한 내용은 `{2}`를 참조하세요." + }, + "workspace": { + "compareWithEachOther": "서로 비교하기", + "confirmDeletePermanently.description": "휴지통을 사용하여 \"{0}\"를 삭제하지 못했습니다. 대신 영구 삭제를 하시겠습니까?", + "confirmDeletePermanently.solution": "환경설정에서 휴지통 사용을 비활성화할 수 있습니다.", + "confirmDeletePermanently.title": "파일 삭제 중 오류 발생", + "confirmMessage.delete": "다음 파일을 정말 삭제하시겠습니까?", + "confirmMessage.dirtyMultiple": "저장되지 않은 변경 사항이 있는 {0} 파일을 정말 삭제하시겠습니까?", + "confirmMessage.dirtySingle": "저장되지 않은 변경 사항이 있는 {0} 을 정말 삭제하시겠습니까?", + "confirmMessage.uriMultiple": "선택한 {0} 파일을 모두 삭제하시겠습니까?", + "confirmMessage.uriSingle": "{0} 을 삭제하시겠습니까?", + "duplicate": "중복", + "failSaveAs": "현재 위젯에 대해 \"{0}\"를 실행할 수 없습니다.", + "newFilePlaceholder": "파일 이름", + "newFolderPlaceholder": "폴더 이름", + "noErasure": "참고: 디스크에서 아무것도 지워지지 않습니다.", + "openRecentPlaceholder": "열려는 워크스페이스의 이름을 입력합니다.", + "openRecentWorkspace": "최근 작업 공간 열기...", + "preserveWindow": "현재 창에서 작업 공간 열기를 활성화합니다.", + "removeFolder": "작업 공간에서 다음 폴더를 제거하시겠습니까?", + "removeFolders": "작업 공간에서 다음 폴더를 제거하시겠습니까?", + "trashTitle": "{0} 을 휴지통으로 이동", + "trustEmptyWindow": "빈 작업 공간을 기본적으로 신뢰할지 여부를 제어합니다.", + "trustEnabled": "워크스페이스 신뢰의 사용 여부를 제어합니다. 비활성화하면 모든 워크스페이스가 신뢰됩니다.", + "trustRequest": "확장 프로그램에서 워크스페이스 신뢰를 요청하지만 해당 API가 아직 완전히 지원되지 않습니다. 이 워크스페이스를 신뢰하시겠습니까?", + "untitled-cleanup": "제목이 없는 작업 공간 파일이 많이 있는 것 같습니다. {0} 을 확인하여 사용하지 않는 파일을 제거하세요.", + "workspaceFolderAdded": "여러 루트가 있는 워크스페이스가 만들어졌습니다. 워크스페이스 구성을 파일로 저장하시겠습니까?", + "workspaceFolderAddedTitle": "워크스페이스에 폴더 추가" + } + } +} diff --git a/packages/core/i18n/nls.pl.json b/packages/core/i18n/nls.pl.json index 3c71f0f27a011..189aa3f86f263 100644 --- a/packages/core/i18n/nls.pl.json +++ b/packages/core/i18n/nls.pl.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "Otwórz widok konfiguracji AI", + "aiHistory:open": "Otwórz widok historii AI", "debug.breakpoint.editCondition": "Warunek edycji...", + "notebook.cell.changeToCode": "Zmień komórkę na kod", + "notebook.cell.changeToMarkdown": "Zmień komórkę na Mardown", + "notebook.cell.insertMarkdownCellAbove": "Wstaw komórkę Markdown powyżej", + "notebook.cell.insertMarkdownCellBelow": "Wstaw komórkę Markdown poniżej", "terminal:new:profile": "Tworzenie nowego zintegrowanego terminalu z profilu", "terminal:profile:default": "Wybierz domyślny profil terminala", "theia": { @@ -7,6 +13,33 @@ "noCallers": "Nie wykryto żadnych rozmówców.", "open": "Hierarchia zaproszeń otwartych" }, + "collaboration": { + "collaborate": "Współpraca", + "collaboration": "Współpraca", + "collaborationWorkspace": "Przestrzeń robocza do współpracy", + "connected": "Połączony", + "connectedSession": "Połączenie z sesją współpracy", + "copiedInvitation": "Kod zaproszenia skopiowany do schowka.", + "copyAgain": "Kopiuj ponownie", + "createRoom": "Utwórz nową sesję współpracy", + "creatingRoom": "Tworzenie sesji", + "end": "Zakończenie sesji współpracy", + "endDetail": "Zakończenie sesji, zaprzestanie udostępniania treści i odebranie dostępu innym osobom.", + "enterCode": "Wprowadź kod sesji współpracy", + "failedCreate": "Nie udało się utworzyć miejsca: {0}", + "failedJoin": "Nie udało się dołączyć do pokoju: {0}", + "invite": "Zaproś innych", + "inviteDetail": "Skopiuj kod zaproszenia, aby udostępnić go innym osobom i dołączyć do sesji.", + "joinRoom": "Dołącz do sesji współpracy", + "joiningRoom": "Sesja dołączania", + "leave": "Zostaw sesję współpracy", + "leaveDetail": "Rozłączenie się z bieżącą sesją współpracy i zamknięcie obszaru roboczego.", + "selectCollaboration": "Wybierz opcję współpracy", + "sharedSession": "Wspólna sesja współpracy", + "startSession": "Rozpoczęcie lub dołączenie do sesji współpracy", + "userWantsToJoin": "Użytkownik '{0}' chce dołączyć do pokoju współpracy", + "whatToDo": "Co chciałbyś zrobić z innymi współpracownikami?" + }, "core": { "about": { "compatibility": "{0} Zgodność", @@ -65,6 +98,13 @@ "next": "Następny (w dół)", "previous": "Poprzedni (Up)" }, + "secondaryWindow": { + "alwaysOnTop": "Po włączeniu tej opcji okno dodatkowe pozostaje nad wszystkimi innymi oknami, w tym nad oknami różnych aplikacji.", + "description": "Ustawia początkową pozycję i rozmiar wyodrębnionego okna pomocniczego.", + "fullSize": "Pozycja i rozmiar wyodrębnionego widżetu będą takie same jak uruchomionej aplikacji Theia.", + "halfWidth": "Pozycja i rozmiar wyodrębnionego widżetu będą równe połowie szerokości uruchomionej aplikacji Theia.", + "originalSize": "Pozycja i rozmiar wyodrębnionego widżetu będą takie same jak oryginalnego widżetu." + }, "silentNotifications": "Określa, czy wyłączyć wyskakujące okienka powiadomień.", "tabDefaultSize": "Określa domyślny rozmiar dla zakładek.", "tabMaximize": "Określa, czy karty mają być maksymalizowane po dwukrotnym kliknięciu.", @@ -94,14 +134,32 @@ "toggleTracing": "Włączanie/wyłączanie śledzenia komunikacji z adapterami debugowania" }, "editor": { + "diffEditor.wordWrap2": "Linie będą zawijane zgodnie z ustawieniem `#editor.wordWrap#`.", "dirtyEncoding": "Plik jest zabrudzony. Proszę zapisać go najpierw przed ponownym otwarciem z innym kodowaniem.", - "editor.codeActionWidget.showHeaders": "Włączenie/wyłączenie pokazywania nagłówków grup w menu akcji kodu.", - "editor.experimental.pasteActions.enabled": "Włączanie/wyłączanie edycji z rozszerzeń podczas wklejania.", + "editor.accessibilitySupport0": "Użyj interfejsów API platformy, aby wykryć podłączenie czytnika ekranu.", + "editor.accessibilitySupport1": "Optymalizacja pod kątem korzystania z czytnika ekranu", + "editor.accessibilitySupport2": "Załóżmy, że czytnik ekranu nie jest podłączony", + "editor.bracketPairColorization.enabled": "Kontroluje, czy kolorowanie par nawiasów jest włączone, czy nie. Użyj `#workbench.colorCustomizations#`, aby zastąpić kolory podświetlenia nawiasów.", + "editor.codeActionWidget.includeNearbyQuickfixes": "Włączenie/wyłączenie wyświetlania najbliższego quickfixa w linii, gdy nie jest on aktualnie w diagnostyce.", + "editor.cursorSurroundingLinesStyle": "Kontroluje, kiedy `#cursorSurroundingLines#` powinien być wymuszany.", + "editor.detectIndentation": "Kontroluje, czy `#editor.tabSize#` i `#editor.insertSpaces#` będą automatycznie wykrywane podczas otwierania pliku na podstawie jego zawartości.", + "editor.dropIntoEditor.enabled": "Kontroluje, czy można przeciągnąć i upuścić plik do edytora tekstu, przytrzymując `shift` (zamiast otwierać plik w edytorze).", "editor.formatOnSaveMode.modificationsIfAvailable": "Spowoduje próbę sformatowania tylko modyfikacji (wymaga kontroli źródła). Jeśli kontrola źródła nie może być użyta, sformatowany zostanie cały plik.", + "editor.hover.hidingDelay": "Kontroluje opóźnienie w milisekundach, po którym hover zostanie ukryty. Wymaga włączenia `editor.hover.sticky`.", "editor.inlayHints.enabled1": "Podpowiedzi są domyślnie wyświetlane i ukrywają się po przytrzymaniu `Ctrl+Alt`.", "editor.inlayHints.enabled2": "Podpowiedzi są domyślnie ukryte i pokazują się po przytrzymaniu `Ctrl+Alt`.", + "editor.inlayHints.fontFamily": "Kontroluje rodzinę czcionek podpowiedzi inlay w edytorze. Gdy ustawiona na pustą, używana jest `#editor.fontFamily#`.", + "editor.inlayHints.fontSize": "Kontroluje rozmiar czcionki podpowiedzi inlay w edytorze. Domyślnie używana jest wartość `#editor.fontSize#`, gdy skonfigurowana wartość jest mniejsza niż `5` lub większa niż rozmiar czcionki edytora.", + "editor.insertSpaces": "Wstawia spacje po naciśnięciu `Tab`. To ustawienie jest nadpisywane na podstawie zawartości pliku, gdy włączona jest funkcja `#editor.detectIndentation#`.", + "editor.occurrencesHighlight": "Kontroluje, czy edytor powinien podświetlać wystąpienia symboli semantycznych.", "editor.quickSuggestions": "Kontroluje czy sugestie powinny być automatycznie wyświetlane podczas pisania. Można to kontrolować w przypadku wpisywania komentarzy, ciągów znaków i innego kodu. Szybkie sugestie mogą być skonfigurowane tak, aby pokazywały się jako tekst widma lub z widżetem sugestii. Należy również pamiętać o ustawieniu '#editor.suggestOnTriggerCharacters#', które kontroluje czy sugestie są uruchamiane przez znaki specjalne.", - "editor.suggest.matchOnWordStartOnly": "Kiedy włączone filtrowanie IntelliSense wymaga, aby pierwszy znak pasował na początku słowa, np. `c` na `Console` lub `WebContext` ale _nie_ na `description`. Kiedy wyłączone IntelliSense pokaże więcej wyników, ale nadal sortuje je według jakości dopasowania.", + "editor.stickyScroll.scrollWithEditor": "Włącza przewijanie widżetu sticky scroll za pomocą poziomego paska przewijania edytora.", + "editor.suggestFontSize": "Rozmiar czcionki dla widżetu sugestii. Gdy ustawione na `0`, używana jest wartość `#editor.fontSize#`.", + "editor.suggestLineHeight": "Wysokość linii dla widżetu sugestii. Gdy ustawione na `0`, używana jest wartość `#editor.lineHeight#`. Minimalna wartość to 8.", + "editor.tabSize": "Liczba spacji równa tabulatorowi. To ustawienie jest nadpisywane na podstawie zawartości pliku, gdy włączona jest funkcja `#editor.detectIndentation#`.", + "editor.useTabStops": "Wstawianie i usuwanie białych znaków następuje po tabulatorach.", + "editor.wordBasedSuggestions": "Kontroluje, czy uzupełnienia powinny być obliczane na podstawie słów w dokumencie.", + "editor.wordBasedSuggestionsMode": "Określa, na podstawie których dokumentów uzupełniane są uzupełnienia oparte na słowach.", "files.autoSave": "Steruje funkcją [automatycznego zapisywania](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) edytorów, w których nie zapisano zmian.", "files.autoSave.afterDelay": "Edytor ze zmianami jest automatycznie zapisywany po upływie skonfigurowanego czasu `#files.autoSaveDelay#`.", "files.autoSave.off": "Edytor ze zmianami nigdy nie jest automatycznie zapisywany.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "kilka sekund temu", "addSignedOff": "Dodaj podpisane przez", - "amendReuseMessag": "Aby ponownie użyć ostatniego komunikatu commit, należy nacisnąć 'Enter' lub 'Escape', aby anulować.", + "amendReuseMessage": "Aby ponownie użyć ostatniego komunikatu commit, należy nacisnąć 'Enter' lub 'Escape', aby anulować.", "amendRewrite": "Ponownie napisać poprzednią wiadomość. Wcisnąć 'Enter', aby potwierdzić lub 'Escape', aby anulować.", "checkoutCreateLocalBranchWithName": "Utworzyć nowy oddział lokalny o nazwie: {0}. Wcisnąć 'Enter', aby potwierdzić lub 'Escape', aby anulować.", "checkoutProvideBranchName": "Proszę podać nazwę oddziału.", @@ -333,6 +391,9 @@ "alreadyRunning": "Hostowana instancja jest już uruchomiona.", "debugInstance": "Instancja debugowania", "debugMode": "Używanie inspect lub inspect-brk do debugowania Node.js", + "debugPorts": { + "debugPort": "Port używany do debugowania Node.js tego serwera" + }, "devHost": "Gospodarz ds. rozwoju", "failed": "Failed to run hosted plugin instance: {0}", "hostedPlugin": "Wtyczka hostowana", @@ -364,6 +425,10 @@ "webviewTrace": "Kontroluje śledzenie komunikacji z webviews.", "webviewWarnIfUnsecure": "Ostrzega użytkowników, że widoki internetowe są obecnie wdrażane w sposób niezabezpieczony." }, + "preferences": { + "hostedPlugin": "Hostowana wtyczka", + "toolbar": "Pasek narzędzi" + }, "preview": { "openByDefault": "Domyślnie otwiera podgląd zamiast edytora." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "ukryte", "config.untrackedChanges.mixed": "mieszane", "config.untrackedChanges.separate": "oddzielna strona", + "dirtyDiff": { + "close": "Zamknij widok podglądu zmian" + }, "history": "Historia", "noRepositoryFound": "Nie znaleziono repozytorium", "unamend": "Zmienić", @@ -401,8 +469,7 @@ "extract-widget": "Przenieś widok do okna podrzędnego" }, "shell-area": { - "secondary": "Drugie okno", - "top": "Top" + "secondary": "Drugie okno" }, "task": { "attachTask": "Dołącz zadanie...", @@ -422,6 +489,7 @@ "profilePath": "Ścieżka powłoki, której używa ten profil.", "profiles": "Profile, które mają być prezentowane podczas tworzenia nowego terminala. Ustaw właściwość path ręcznie z opcjonalnymi argumentami.\nUstaw istniejący profil na `null`, aby ukryć profil z listy, na przykład: `\"{0}\": null`.", "rendererType": "Kontroluje sposób renderowania terminala.", + "rendererTypeDeprecationMessage": "Typ renderowania nie jest już obsługiwany jako opcja.", "selectProfile": "Wybrać profil dla nowego terminalu", "shell.deprecated": "Jest to przestarzałe, nowym zalecanym sposobem konfiguracji domyślnej powłoki jest utworzenie profilu terminala w 'terminal.integrated.profiles.{0}' i ustawienie jego nazwy jako domyślnej w 'terminal.integrated.defaultProfile.{0}.'", "shellArgsLinux": "Argumenty wiersza poleceń do użycia w terminalu Linuksa.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Anulowanie wszystkich testów", + "stackFrameAt": "na", "testRunDefaultName": "{0} bieg {1}", "testRuns": "Przebiegi testowe" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Hierarchia nadtypów" }, "vsx-registry": { + "confirmDialogMessage": "Rozszerzenie \"{0}\" jest niezweryfikowane i może stanowić zagrożenie dla bezpieczeństwa.", + "confirmDialogTitle": "Czy na pewno chcesz kontynuować instalację?", "downloadCount": "Pobierz licz: {0}", "errorFetching": "Błąd pobierania rozszerzeń.", "errorFetchingConfigurationHint": "Może to być spowodowane problemami z konfiguracją sieci.", "failedInstallingVSIX": "Nie udało się zainstalować {0} z VSIX.", "invalidVSIX": "Wybrany plik nie jest prawidłowym pluginem \"*.vsix\".", "license": "Licencja: {0}", + "onlyShowVerifiedExtensionsDescription": "Dzięki temu strona {0} będzie wyświetlać tylko zweryfikowane rozszerzenia.", + "onlyShowVerifiedExtensionsTitle": "Pokaż tylko zweryfikowane rozszerzenia", "recommendedExtensions": "Lista nazw rozszerzeń zalecanych do użycia w tym obszarze roboczym.", "searchPlaceholder": "Wyszukaj rozszerzenia w {0}.", "showInstalled": "Pokaż zainstalowane rozszerzenia", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "Czy naprawdę chcesz usunąć wszystkie {0} zaznaczone pliki?", "confirmMessage.uriSingle": "Czy naprawdę chcesz usunąć {0}?", "duplicate": "Duplikat", - "failApply": "Nie można zastosować zmian do nowego pliku", "failSaveAs": "Nie można uruchomić \"{0}\" dla bieżącego widżetu.", "newFilePlaceholder": "Nazwa pliku", "newFolderPlaceholder": "Nazwa folderu", diff --git a/packages/core/i18n/nls.pt-br.json b/packages/core/i18n/nls.pt-br.json index 989f735cea0c5..2fba038a91fe7 100644 --- a/packages/core/i18n/nls.pt-br.json +++ b/packages/core/i18n/nls.pt-br.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "Abrir a visualização de Configuração de IA", + "aiHistory:open": "Abrir a visualização do histórico de IA", "debug.breakpoint.editCondition": "Editar condição...", + "notebook.cell.changeToCode": "Alterar célula para código", + "notebook.cell.changeToMarkdown": "Mudança de célula para Mardown", + "notebook.cell.insertMarkdownCellAbove": "Inserir célula Markdown acima", + "notebook.cell.insertMarkdownCellBelow": "Inserir célula Markdown abaixo", "terminal:new:profile": "Criar um novo terminal integrado a partir de um perfil", "terminal:profile:default": "Escolha o Perfil do Terminal padrão", "theia": { @@ -7,6 +13,33 @@ "noCallers": "Nenhum chamador foi detectado.", "open": "Hierarquia de Chamadas Abertas" }, + "collaboration": { + "collaborate": "Colaborar", + "collaboration": "Colaboração", + "collaborationWorkspace": "Espaço de trabalho de colaboração", + "connected": "Conectado", + "connectedSession": "Conectado a uma sessão de colaboração", + "copiedInvitation": "Código do convite copiado para a área de transferência.", + "copyAgain": "Copiar novamente", + "createRoom": "Criar nova sessão de colaboração", + "creatingRoom": "Criação de sessão", + "end": "Encerrar a sessão de colaboração", + "endDetail": "Encerrar a sessão, interromper o compartilhamento de conteúdo e revogar o acesso de outras pessoas.", + "enterCode": "Digite o código da sessão de colaboração", + "failedCreate": "Falha ao criar a sala: {0}", + "failedJoin": "Falha ao entrar na sala: {0}", + "invite": "Convidar outras pessoas", + "inviteDetail": "Copie o código do convite para compartilhá-lo com outras pessoas e participar da sessão.", + "joinRoom": "Participe da sessão de colaboração", + "joiningRoom": "Sessão de ingresso", + "leave": "Sair da sessão de colaboração", + "leaveDetail": "Desconecte-se da sessão de colaboração atual e feche o espaço de trabalho.", + "selectCollaboration": "Selecione a opção de colaboração", + "sharedSession": "Compartilhou uma sessão de colaboração", + "startSession": "Iniciar ou participar de uma sessão de colaboração", + "userWantsToJoin": "O usuário '{0}' deseja participar da sala de colaboração", + "whatToDo": "O que você gostaria de fazer com outros colaboradores?" + }, "core": { "about": { "compatibility": "{0} Compatibilidade", @@ -65,6 +98,13 @@ "next": "Próximo (Abaixo)", "previous": "Anterior (Para cima)" }, + "secondaryWindow": { + "alwaysOnTop": "Quando ativada, a janela secundária fica acima de todas as outras janelas, inclusive as de aplicativos diferentes.", + "description": "Define a posição inicial e o tamanho da janela secundária extraída.", + "fullSize": "A posição e o tamanho do widget extraído serão os mesmos do aplicativo Theia em execução.", + "halfWidth": "A posição e o tamanho do widget extraído terão a metade da largura do aplicativo Theia em execução.", + "originalSize": "A posição e o tamanho do widget extraído serão os mesmos do widget original." + }, "silentNotifications": "Controla se devem ser suprimidas as popups de notificação.", "tabDefaultSize": "Especifica o tamanho padrão das guias.", "tabMaximize": "Controla se é necessário maximizar as abas com duplo clique.", @@ -94,14 +134,32 @@ "toggleTracing": "Habilitar/desabilitar as comunicações de rastreamento com adaptadores de depuração" }, "editor": { + "diffEditor.wordWrap2": "As linhas serão quebradas de acordo com a configuração `#editor.wordWrap#`.", "dirtyEncoding": "O arquivo está sujo. Por favor, salve-o primeiro antes de reabri-lo com outra codificação.", - "editor.codeActionWidget.showHeaders": "Habilitar/desabilitar mostrar os cabeçalhos de grupo no menu de ação de código.", - "editor.experimental.pasteActions.enabled": "Ativar/desativar a execução de edições de extensões ao colar.", + "editor.accessibilitySupport0": "Use APIs da plataforma para detectar quando um leitor de tela está conectado", + "editor.accessibilitySupport1": "Otimizar para uso com um leitor de tela", + "editor.accessibilitySupport2": "Suponha que um leitor de tela não esteja conectado", + "editor.bracketPairColorization.enabled": "Controla se a colorização de pares de colchetes está ativada ou não. Use `#workbench.colorCustomizations#` para substituir as cores de destaque dos colchetes.", + "editor.codeActionWidget.includeNearbyQuickfixes": "Ativar/desativar a exibição do reparo rápido mais próximo em uma linha quando não estiver em um diagnóstico no momento.", + "editor.cursorSurroundingLinesStyle": "Controla quando o `#cursorSurroundingLines#` deve ser aplicado.", + "editor.detectIndentation": "Controla se `#editor.tabSize#` e `#editor.insertSpaces#` serão detectados automaticamente quando um arquivo for aberto com base no conteúdo do arquivo.", + "editor.dropIntoEditor.enabled": "Controla se você pode arrastar e soltar um arquivo em um editor de texto mantendo pressionada a tecla `shift` (em vez de abrir o arquivo em um editor).", "editor.formatOnSaveMode.modificationsIfAvailable": "Tentará formatar modificações apenas (requer controle de fonte). Se o controle da fonte não puder ser usado, então o arquivo inteiro será formatado.", + "editor.hover.hidingDelay": "Controla o atraso, em milissegundos, após o qual o hover fica oculto. Requer que a opção `editor.hover.sticky` esteja ativada.", "editor.inlayHints.enabled1": "Dicas de Inlay estão mostrando por padrão e se escondem ao segurar `Ctrl+Alt`.", "editor.inlayHints.enabled2": "Dicas de Inlay são ocultadas por padrão e mostram quando se segura `Ctrl+Alt`.", + "editor.inlayHints.fontFamily": "Controla a família de fontes das dicas de inlay no editor. Quando definido como vazio, a `#editor.fontFamily#` é usada.", + "editor.inlayHints.fontSize": "Controla o tamanho da fonte das dicas de inlay no editor. Por padrão, o `#editor.fontSize#` é usado quando o valor configurado é menor que `5` ou maior que o tamanho da fonte do editor.", + "editor.insertSpaces": "Inserir espaços ao pressionar `Tab`. Essa configuração é substituída com base no conteúdo do arquivo quando `#editor.detectIndentation#` está ativado.", + "editor.occurrencesHighlight": "Controla se o editor deve destacar as ocorrências de símbolos semânticos.", "editor.quickSuggestions": "Controla se as sugestões devem aparecer automaticamente durante a digitação. Isto pode ser controlado para digitação de comentários, strings, e outros códigos. A sugestão rápida pode ser configurada para aparecer como texto fantasma ou com o widget de sugestão. Esteja ciente também do '#editor.suggestOnTriggerCharacters#'-setting que controla se as sugestões são acionadas por caracteres especiais.", - "editor.suggest.matchOnWordStartOnly": "Quando ativada, a filtragem IntelliSense requer que o primeiro caractere corresponda em um início de palavra, por exemplo `c` em `Console` ou `Contexto Web` mas _não_ em `descrição`. Quando desabilitado, o IntelliSense mostrará mais resultados, mas ainda assim os classifica pela qualidade da correspondência.", + "editor.stickyScroll.scrollWithEditor": "Ative a rolagem do widget de rolagem fixa com a barra de rolagem horizontal do editor.", + "editor.suggestFontSize": "Tamanho da fonte para o widget de sugestão. Quando definido como `0`, o valor de `#editor.fontSize#` é usado.", + "editor.suggestLineHeight": "Altura da linha para o widget de sugestão. Quando definido como `0`, o valor de `#editor.lineHeight#` é usado. O valor mínimo é 8.", + "editor.tabSize": "O número de espaços a que uma tabulação é igual. Essa configuração é substituída com base no conteúdo do arquivo quando `#editor.detectIndentation#` está ativado.", + "editor.useTabStops": "A inserção e a exclusão de espaços em branco seguem as paradas de tabulação.", + "editor.wordBasedSuggestions": "Controla se as conclusões devem ser computadas com base nas palavras do documento.", + "editor.wordBasedSuggestionsMode": "Controla a partir de quais documentos as conclusões baseadas em palavras são computadas.", "files.autoSave": "Controles [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) de editores que não salvaram mudanças.", "files.autoSave.afterDelay": "Um editor com mudanças é automaticamente salvo após o `#files.autoSaveDelay#` configurado.", "files.autoSave.off": "Um editor com mudanças nunca é salvo automaticamente.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "alguns segundos atrás", "addSignedOff": "Adicionar Signed-off-by", - "amendReuseMessag": "Para reutilizar a última mensagem de compromisso, pressione 'Enter' ou 'Escape' para cancelar.", + "amendReuseMessage": "Para reutilizar a última mensagem de compromisso, pressione 'Enter' ou 'Escape' para cancelar.", "amendRewrite": "Reescrever mensagem de compromisso anterior. Pressione 'Enter' para confirmar ou 'Escape' para cancelar.", "checkoutCreateLocalBranchWithName": "Criar uma nova filial local com o nome: {0}. Pressione 'Enter' para confirmar ou 'Escape' para cancelar.", "checkoutProvideBranchName": "Por favor, forneça um nome de filial.", @@ -333,6 +391,9 @@ "alreadyRunning": "A instância anfitriã já está funcionando.", "debugInstance": "Instância de depuração", "debugMode": "Usando a inspeção ou a inspeção-brk para o Node.js debug", + "debugPorts": { + "debugPort": "Porta a ser usada para a depuração do Node.js deste servidor" + }, "devHost": "Anfitrião do desenvolvimento", "failed": "Falha na execução da instância de plugin hospedado: {0}", "hostedPlugin": "Plugin hospedado", @@ -364,6 +425,10 @@ "webviewTrace": "Controla o rastreamento da comunicação com visualizações na web.", "webviewWarnIfUnsecure": "Adverte os usuários de que as visualizações na web são atualmente implantadas de forma insegura." }, + "preferences": { + "hostedPlugin": "Plug-in hospedado", + "toolbar": "Barra de ferramentas" + }, "preview": { "openByDefault": "Abrir a visualização em vez do editor por padrão." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "oculto", "config.untrackedChanges.mixed": "misto", "config.untrackedChanges.separate": "separado", + "dirtyDiff": { + "close": "Fechar Alterar Vista panorâmica" + }, "history": "História", "noRepositoryFound": "Não foi encontrado nenhum repositório", "unamend": "Unamend", @@ -401,8 +469,7 @@ "extract-widget": "Mover vista para a janela secundária" }, "shell-area": { - "secondary": "Janela Secundária", - "top": "Topo" + "secondary": "Janela Secundária" }, "task": { "attachTask": "Anexar Tarefa...", @@ -422,6 +489,7 @@ "profilePath": "O caminho da casca que este perfil utiliza.", "profiles": "Os perfis a serem apresentados ao criar um novo terminal. Definir a propriedade do caminho manualmente com args opcionais.\nDefinir um perfil existente para 'nulo' para ocultar o perfil da lista, por exemplo: `\"{0}\": nulo'.", "rendererType": "Controla como o terminal é renderizado.", + "rendererTypeDeprecationMessage": "O tipo de renderizador não é mais suportado como uma opção.", "selectProfile": "Selecione um perfil para o novo terminal", "shell.deprecated": "Isto é depreciado, a nova forma recomendada para configurar sua shell padrão é criando um perfil de terminal em 'terminal.integrated.profiles.{0}' e definindo seu nome de perfil como o padrão em 'terminal.integrated.defaultProfile'.{0}.", "shellArgsLinux": "Os argumentos de linha de comando a serem usados quando no terminal Linux.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Cancelar todas as execuções de teste", + "stackFrameAt": "em", "testRunDefaultName": "{0} executar {1}", "testRuns": "Execuções de teste" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Supertipo Hierarquia" }, "vsx-registry": { + "confirmDialogMessage": "A extensão \"{0}\" não foi verificada e pode representar um risco à segurança.", + "confirmDialogTitle": "Tem certeza de que deseja prosseguir com a instalação?", "downloadCount": "Contagem de downloads: {0}", "errorFetching": "Extensões de erro de busca.", "errorFetchingConfigurationHint": "Isso pode ser causado por problemas de configuração de rede.", "failedInstallingVSIX": "Falha na instalação {0} da VSIX.", "invalidVSIX": "O arquivo selecionado não é um plugin \"*.vsix\" válido.", "license": "Licença: {0}", + "onlyShowVerifiedExtensionsDescription": "Isso permite que o site {0} mostre apenas as extensões verificadas.", + "onlyShowVerifiedExtensionsTitle": "Mostrar apenas as extensões verificadas", "recommendedExtensions": "Uma lista dos nomes das extensões recomendadas para uso neste espaço de trabalho.", "searchPlaceholder": "Pesquisar extensões em {0}", "showInstalled": "Mostrar extensões instaladas", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "Você realmente quer apagar todos os {0} arquivos selecionados?", "confirmMessage.uriSingle": "Você realmente quer apagar {0}?", "duplicate": "Duplicata", - "failApply": "Não foi possível aplicar alterações ao novo arquivo", "failSaveAs": "Não é possível executar \"{0}\" para o widget atual.", "newFilePlaceholder": "Nome do arquivo", "newFolderPlaceholder": "Nome da pasta", diff --git a/packages/core/i18n/nls.pt-pt.json b/packages/core/i18n/nls.pt-pt.json deleted file mode 100644 index 05b4bd72fd2e8..0000000000000 --- a/packages/core/i18n/nls.pt-pt.json +++ /dev/null @@ -1,513 +0,0 @@ -{ - "debug.breakpoint.editCondition": "Editar condição...", - "terminal:new:profile": "Criar Novo Terminal Integrado a partir de um Perfil", - "terminal:profile:default": "Escolha o Perfil Terminal padrão", - "theia": { - "callhierarchy": { - "noCallers": "Não foram detectadas pessoas que tenham telefonado.", - "open": "Hierarquia de Chamadas Abertas" - }, - "core": { - "about": { - "compatibility": "{0} Compatibilidade", - "defaultApi": "Padrão {0} API", - "version": "Versão" - }, - "common": { - "closeAll": "Fechar todos os separadores", - "closeAllTabMain": "Fechar todos os separadores na área principal", - "closeOtherTabMain": "Fechar outros separadores na área principal", - "closeOthers": "Fechar outros separadores", - "closeRight": "Fechar abas para a direita", - "closeTab": "Aba fechar", - "closeTabMain": "Fechar separador na área principal", - "collapseAllTabs": "Colapso de todos os painéis laterais", - "collapseBottomPanel": "Alternar Painel Inferior", - "collapseTab": "Painel lateral de colapso", - "showNextTabGroup": "Mudar para Grupo de Separador Seguinte", - "showNextTabInGroup": "Mudar para o separador seguinte no grupo", - "showPreviousTabGroup": "Mudar para Grupo de Separador Anterior", - "showPreviousTabInGroup": "Mudar para o separador Anterior em Grupo", - "toggleMaximized": "Alternar Maximizado" - }, - "copyInfo": "Abra um ficheiro primeiro para copiar o seu caminho", - "copyWarn": "Utilize o comando de cópia ou o atalho do browser.", - "cutWarn": "Utilize o comando de corte ou o atalho do browser.", - "enhancedPreview": { - "classic": "Apresenta uma pré-visualização simples do separador com informações básicas.", - "enhanced": "Apresentar uma pré-visualização melhorada do separador com informações adicionais.", - "visual": "Apresenta uma pré-visualização visual do separador." - }, - "file": { - "browse": "Navegar" - }, - "highlightModifiedTabs": "Controla se uma borda superior é desenhada ou não em abas de editor modificadas (sujas).", - "keybindingStatus": "{0} foi premido, à espera de mais teclas", - "keyboard": { - "choose": "Escolha o Layout do Teclado", - "chooseLayout": "Escolha um layout de teclado", - "current": "(actual: {0})", - "currentLayout": " - layout actual", - "mac": "Teclados Mac", - "pc": "Teclados de PC", - "tryDetect": "Tente detectar a disposição do teclado a partir da informação do browser e prima as teclas." - }, - "navigator": { - "clipboardWarn": "O acesso à área de transferência foi negado. Verifique a permissão do seu browser.", - "clipboardWarnFirefox": "A API da área de transferência não está disponível. Pode ser activada através da preferência '{0}' na página '{1}'. Em seguida, recarregue o Theia. Note que isso permitirá ao FireFox obter acesso total à área de transferência do sistema." - }, - "offline": "Offline", - "pasteWarn": "Utilize o comando de colar ou o atalho do browser.", - "quitMessage": "Quaisquer alterações não guardadas não serão salvas.", - "resetWorkbenchLayout": "Repor o layout da bancada de trabalho", - "searchbox": { - "close": "Fechar (Escape)", - "next": "Próximo (Para baixo)", - "previous": "Anterior (Para cima)" - }, - "silentNotifications": "Controla se deve suprimir popups de notificação.", - "tabDefaultSize": "Especifica o tamanho predefinido dos separadores.", - "tabMaximize": "Controla se pretende maximizar os separadores com um duplo clique.", - "tabMinimumSize": "Especifica o tamanho mínimo dos separadores.", - "tabShrinkToFit": "Encolher os separadores de acordo com o espaço disponível." - }, - "debug": { - "addConfigurationPlaceholder": "Seleccione a raiz do espaço de trabalho para adicionar configuração a", - "breakpoint": "ponto de interrupção", - "compound-cycle": "Configuração de lançamento '{0}' contém um ciclo consigo mesmo", - "continueAll": "Continuar Tudo", - "copyExpressionValue": "Valor de Expressão da Cópia", - "dataBreakpoint": "ponto de interrupção de dados", - "debugVariableInput": "Conjunto {0} Valor", - "entry": "entrada", - "exception": "exceção", - "functionBreakpoint": "ponto de interrupção da função", - "goto": "goto", - "instruction-breakpoint": "Ponto de Interrupção da Instrução", - "instructionBreakpoint": "ponto de interrupção de instrução", - "missingConfiguration": "Configuração dinâmica '{0}:{1}' está em falta ou não é aplicável", - "pause": "pausa", - "pauseAll": "Pausa Todos", - "reveal": "Revelar", - "step": "passo", - "threads": "Tópicos", - "toggleTracing": "Activar/desactivar as comunicações de rastreio com adaptadores de depuração" - }, - "editor": { - "dirtyEncoding": "O ficheiro está sujo. Por favor guardá-lo primeiro antes de o reabrir com outra codificação.", - "editor.codeActionWidget.showHeaders": "Activar/desactivar mostrar os cabeçalhos de grupo no menu de acção de código.", - "editor.experimental.pasteActions.enabled": "Ativar/desativar a execução de edições de extensões ao colar.", - "editor.formatOnSaveMode.modificationsIfAvailable": "Tentará apenas formatar modificações (requer controlo da fonte). Se o controlo da fonte não puder ser utilizado, então o ficheiro inteiro será formatado.", - "editor.inlayHints.enabled1": "Dicas de Inlay estão a mostrar por defeito e escondem-se quando se segura \"Ctrl+Alt\".", - "editor.inlayHints.enabled2": "Dicas de Inlay são ocultadas por defeito e mostram quando se mantém 'Ctrl+Alt'.", - "editor.quickSuggestions": "Controla se as sugestões devem aparecer automaticamente durante a dactilografia. Isto pode ser controlado para digitação de comentários, strings, e outros códigos. A sugestão rápida pode ser configurada para aparecer como texto fantasma ou com o widget de sugestão. Esteja também ciente do '#editor.suggestOnTriggerCharacters#'-setting que controla se as sugestões são accionadas por caracteres especiais.", - "editor.suggest.matchOnWordStartOnly": "Quando activada, a filtragem IntelliSense requer que o primeiro caractere corresponda num início de palavra, por exemplo `c` em `Console` ou `Contexto Web` mas _não_ em `descrição`. Quando desactivado, o IntelliSense mostrará mais resultados, mas ainda os classifica pela qualidade da correspondência.", - "files.autoSave": "Controla [auto guardar](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) de editores que tenham alterações não guardadas.", - "files.autoSave.afterDelay": "Um editor com alterações é automaticamente guardado após a configuração `#files.autoSaveDelay#``.", - "files.autoSave.off": "Um editor com alterações nunca é automaticamente guardado.", - "files.autoSave.onFocusChange": "Um editor com alterações é automaticamente guardado quando o editor perde o foco.", - "files.autoSave.onWindowChange": "Um editor com alterações é automaticamente guardado quando a janela perde o foco.", - "formatOnSaveTimeout": "Timeout em milissegundos após o qual a formatação que é executada em ficheiro guardado é cancelada.", - "persistClosedEditors": "Controla se deve persistir o histórico do editor fechado para o espaço de trabalho através de recargas de janela.", - "showAllEditors": "Mostrar todos os editores abertos", - "splitHorizontal": "Editor dividido Horizontal", - "splitVertical": "Split Editor Vertical", - "toggleStickyScroll": "Manípulo de rotação" - }, - "file-search": { - "toggleIgnoredFiles": " (Prima {0} para mostrar/ocultar ficheiros ignorados)" - }, - "fileDialog": { - "showHidden": "Mostrar ficheiros ocultos" - }, - "fileSystem": { - "fileResource": { - "overWriteBody": "Quer sobrescrever as alterações feitas a '{0}' no sistema de ficheiros?" - } - }, - "filesystem": { - "copiedToClipboard": "Copiou a ligação de transferência para a área de transferência.", - "copyDownloadLink": "Link para download de cópias", - "dialog": { - "initialLocation": "Ir para o local inicial", - "multipleItemMessage": "Pode seleccionar apenas um item", - "name": "Nome:", - "navigateBack": "Navegar para trás", - "navigateForward": "Navegar para a frente", - "navigateUp": "Navegar para cima de um directório" - }, - "fileResource": { - "binaryFileQuery": "A sua abertura pode demorar algum tempo e pode tornar a IDE pouco reactiva. Quer abrir '{0}' de qualquer forma?", - "binaryTitle": "O ficheiro ou é binário ou utiliza uma codificação de texto não suportada.", - "largeFileTitle": "O ficheiro é demasiado grande ({0}).", - "overwriteTitle": "O ficheiro '{0}' foi alterado no sistema de ficheiros." - }, - "filesExclude": "Configurar padrões globais para excluir ficheiros e pastas. Por exemplo, o Explorador de ficheiros decide quais os ficheiros e pastas a mostrar ou esconder com base nesta configuração.", - "format": "Formato:", - "maxConcurrentUploads": "Número máximo de ficheiros simultâneos a carregar ao carregar vários ficheiros. 0 significa que todos os ficheiros serão carregados ao mesmo tempo.", - "maxFileSizeMB": "Controla o tamanho máximo do ficheiro em MB que é possível abrir.", - "prepareDownload": "A preparar o download...", - "prepareDownloadLink": "Preparar o link para descarregar...", - "processedOutOf": "Processado {0} a partir de {1}", - "replaceTitle": "Substituir ficheiro", - "uploadFiles": "Carregar ficheiros...", - "uploadedOutOf": "Carregado {0} a partir de {1}" - }, - "getting-started": { - "apiComparator": "{0} Compatibilidade API", - "newExtension": "Construir uma nova extensão", - "newPlugin": "Construir um Novo Plugin", - "startup-editor": { - "welcomePage": "Abra a página de boas-vindas, com conteúdo para ajudar a começar a utilizar {0} e as extensões." - } - }, - "git": { - "aFewSecondsAgo": "há alguns segundos", - "addSignedOff": "Adicionar Signed-off-by", - "amendReuseMessag": "Para reutilizar a última mensagem de compromisso, prima 'Enter' ou 'Escape' para cancelar.", - "amendRewrite": "Reescrever mensagem de compromisso anterior. Prima 'Enter' para confirmar ou 'Escape' para cancelar.", - "checkoutCreateLocalBranchWithName": "Criar uma nova filial local com o nome: {0}. Prima 'Enter' para confirmar ou 'Escape' para cancelar.", - "checkoutProvideBranchName": "Por favor, forneça um nome de filial.", - "checkoutSelectRef": "Seleccionar um árbitro para fazer a caixa ou criar uma nova sucursal local:", - "cloneQuickInputLabel": "Por favor, forneça a localização de um repositório Git. Prima 'Enter' para confirmar ou 'Escape' para cancelar.", - "cloneRepository": "Clonar o repositório Git: {0}. Prima 'Enter' para confirmar ou 'Escape' para cancelar.", - "compareWith": "Comparar com...", - "compareWithBranchOrTag": "Escolher um ramo ou etiqueta para comparar com o ramo actualmente activo {0}:", - "diff": "Diff", - "dirtyDiffLinesLimit": "Não mostrar decorações difusas sujas, se a contagem de linhas do editor exceder este limite.", - "dropStashMessage": "Armazenamento removido com sucesso.", - "editorDecorationsEnabled": "Mostrar decorações de git no editor.", - "fetchPickRemote": "Escolher um comando de onde ir buscar:", - "gitDecorationsColors": "Utilizar decoração a cores no navegador.", - "mergeQuickPickPlaceholder": "Escolher um ramo para se fundir no ramo actualmente activo {0}:", - "missingUserInfo": "Certifique-se de configurar o seu 'user.name' e 'user.email' em git.", - "noHistoryForError": "Não existe um historial disponível para {0}", - "noPreviousCommit": "Nenhum compromisso anterior para emendar", - "noRepositoriesSelected": "Não foram seleccionados repositórios.", - "prepositionIn": "em", - "repositoryNotInitialized": "Repositório {0} ainda não foi inicializado.", - "stashChanges": "Mudanças de stock. Prima 'Enter' para confirmar ou 'Escape' para cancelar.", - "stashChangesWithMessage": "Mudanças de stock com mensagem: {0}. Prima 'Enter' para confirmar ou 'Escape' para cancelar.", - "tabTitleIndex": "{0} (índice)", - "tabTitleWorkingTree": "{0} (Árvore de trabalho)", - "toggleBlameAnnotations": "Anotações de culpa alternadas" - }, - "keybinding-schema-updater": { - "deprecation": "Utilize antes a cláusula `when`." - }, - "keymaps": { - "addKeybindingTitle": "Adicionar uma ligação de teclas para {0}", - "editKeybinding": "Editar vinculação de teclas...", - "editKeybindingTitle": "Editar encadernação de chaves para {0}", - "editWhenExpression": "Editar quando a expressão...", - "editWhenExpressionTitle": "Editar quando a expressão para {0}", - "keybinding": { - "copy": "Copiar encadernação de teclas", - "copyCommandId": "Copiar ID do comando de ligação de teclas", - "copyCommandTitle": "Copiar vinculação de teclas Título do comando", - "edit": "Editar vinculação de teclas...", - "editWhenExpression": "Editar vinculação de teclas quando a expressão..." - }, - "keybindingCollidesValidation": "ligação de chaves actualmente colide", - "requiredKeybindingValidation": "é necessário um valor de encadernação", - "resetKeybindingConfirmation": "Quer mesmo repor esta encadernação de chave no seu valor por defeito?", - "resetKeybindingTitle": "Repor a encadernação para {0}", - "resetMultipleKeybindingsWarning": "Se existirem múltiplas ligações de teclas para este comando, todas elas serão reiniciadas." - }, - "localize": { - "offlineTooltip": "Não se pode ligar ao backend." - }, - "markers": { - "clearAll": "Limpar tudo", - "noProblems": "Até agora não foram detectados problemas no espaço de trabalho.", - "tabbarDecorationsEnabled": "Mostrar decoradores de problemas (marcadores de diagnóstico) nas barras de tabulação." - }, - "memory-inspector": { - "addressTooltip": "Localização da memória a exibir, um endereço ou expressão a avaliar para um endereço", - "ascii": "ASCII", - "binary": "Binário", - "byteSize": "Tamanho do byte", - "bytesPerGroup": "Bytes Por Grupo", - "closeSettings": "Fechar Definições", - "columns": "Colunas", - "command": { - "createNewMemory": "Criar Novo Inspector de Memória", - "createNewRegisterView": "Criar Nova Vista de Registo", - "followPointer": "Siga o Ponteiro", - "followPointerMemory": "Seguir o Ponto em Memória Inspector", - "resetValue": "Valor de reinicialização", - "showRegister": "Mostrar Registo em Memória Inspector", - "viewVariable": "Mostrar Variable in Memory Inspector" - }, - "data": "Dados", - "decimal": "Decimal", - "diff": { - "label": "Dif: {0}" - }, - "diff-widget": { - "offset-label": "{0} Offset", - "offset-title": "Bytes para compensar a memória de {0}" - }, - "editable": { - "apply": "Aplicar alterações", - "clear": "Mudanças claras" - }, - "endianness": "Endianness", - "extraColumn": "Coluna Extra", - "groupsPerRow": "Grupos por fila", - "hexadecimal": "Hexadecimal", - "length": "Comprimento", - "lengthTooltip": "Número de bytes a buscar, em decimal ou hexadecimal", - "memory": { - "addressField": { - "memoryReadError": "Introduzir um endereço ou expressão no campo Localização." - }, - "freeze": "Vista de memória congelada", - "hideSettings": "Esconder Painel de Definições", - "readError": { - "bounds": "Limites de memória excedidos, o resultado será truncado.", - "noContents": "Não existe actualmente nenhum conteúdo de memória disponível." - }, - "readLength": { - "memoryReadError": "Introduza um comprimento (número decimal ou hexadecimal) no campo Comprimento." - }, - "showSettings": "Mostrar Painel de Ajustes", - "unfreeze": "Vista da memória de descongelamento", - "userError": "Havia uma memória de busca de erros." - }, - "memoryCategory": "Inspector de Memória", - "memoryInspector": "Inspector de Memória", - "memoryTitle": "Memória", - "octal": "Octal", - "offset": "Offset", - "offsetTooltip": "Offset a ser adicionado ao local da memória actual, quando se navega", - "provider": { - "localsError": "Não é possível ler variáveis locais. Não há sessão de depuração activa.", - "readError": "Não é possível ler a memória. Sem sessão de depuração activa.", - "writeError": "Não é possível escrever memória. Sem sessão de depuração activa." - }, - "register": "Registe-se", - "register-widget": { - "filter-placeholder": "Filtro (começa por)" - }, - "registerReadError": "Havia um registo de busca de erros.", - "registers": "Registos", - "toggleComparisonWidgetVisibility": "Visibilidade do widget de comparação de alternância", - "utils": { - "afterBytes": "Deve carregar memória em ambos os widgets que gostaria de comparar. {0} não tem memória carregada.", - "bytesMessage": "Deve carregar memória em ambos os widgets que gostaria de comparar. {0} não tem memória carregada." - } - }, - "messages": { - "notificationTimeout": "As notificações informativas serão ocultadas após este intervalo de tempo.", - "toggleNotifications": "Notificações de alternância" - }, - "mini-browser": { - "typeUrl": "Digite um URL" - }, - "monaco": { - "noSymbolsMatching": "Sem correspondência de símbolos", - "typeToSearchForSymbols": "Tipo para pesquisa de símbolos" - }, - "navigator": { - "autoReveal": "Auto Revelação", - "clipboardWarn": "O acesso à área de transferência foi negado. Verifique a permissão do seu browser.", - "clipboardWarnFirefox": "A API da área de transferência não está disponível. Pode ser activada através da preferência '{0}' na página '{1}'. Em seguida, recarregue o Theia. Note que isso permitirá ao FireFox obter acesso total à área de transferência do sistema.", - "refresh": "Actualizar no Explorer", - "reveal": "Revelar no Explorer", - "toggleHiddenFiles": "Alternar ficheiros escondidos" - }, - "output": { - "clearOutputChannel": "Canal de saída transparente...", - "closeOutputChannel": "Fechar canal de saída...", - "hiddenChannels": "Canais ocultos", - "hideOutputChannel": "Esconder canal de saída...", - "maxChannelHistory": "O número máximo de entradas num canal de saída.", - "outputChannels": "Canais de saída", - "showOutputChannel": "Mostrar canal de saída..." - }, - "plugin": { - "blockNewTab": "O seu navegador impediu a abertura de um novo separador" - }, - "plugin-dev": { - "alreadyRunning": "A instância anfitriã já está a funcionar.", - "debugInstance": "Tribunal de depuração", - "debugMode": "Usando inspeccionar ou inspeccionar-brk para Node.js debug", - "devHost": "Anfitrião do Desenvolvimento", - "failed": "Falha na execução de uma instância de plugin hospedado: {0}", - "hostedPlugin": "Plugin hospedado", - "hostedPluginRunning": "Plugin hospedado: A funcionar", - "hostedPluginStarting": "Plugin hospedado: Início", - "hostedPluginStopped": "Plugin hospedado: Parado", - "hostedPluginWatching": "Plugin hospedado: Observando", - "instanceTerminated": "{0} foi terminado", - "launchOutFiles": "O conjunto de padrões globais para localização de ficheiros JavaScript gerados (`${pluginPath}` será substituído pelo caminho real do plugin).", - "noValidPlugin": "A pasta especificada não contém um plugin válido.", - "notRunning": "A instância anfitriã não está a funcionar.", - "pluginFolder": "A pasta Plugin está definida para: {0}", - "preventedNewTab": "O seu navegador impediu a abertura de um novo separador", - "restartInstance": "Reinício da instância", - "running": "A instância anfitriã está a funcionar em:", - "select": "Seleccione", - "selectPath": "Seleccione o caminho", - "startInstance": "Instância inicial", - "starting": "Iniciar servidor de instância hospedado ...", - "stopInstance": "Instância de paragem", - "unknownTerminated": "A instância foi encerrada", - "watchMode": "Executar o watcher em plugin em desenvolvimento" - }, - "plugin-ext": { - "authentication-main": { - "loginTitle": "Iniciar sessão" - }, - "plugins": "Plugins", - "webviewTrace": "Controla o rastreio da comunicação com visualizações na web.", - "webviewWarnIfUnsecure": "Adverte os utilizadores de que as visualizações da web estão actualmente implantadas de forma insegura." - }, - "preview": { - "openByDefault": "Abrir a pré-visualização em vez do editor por defeito." - }, - "property-view": { - "created": "Criado em", - "directory": "Directório", - "lastModified": "Última modificação", - "location": "Localização", - "noProperties": "Não existem propriedades disponíveis.", - "properties": "Imóveis", - "size": "Tamanho", - "symbolicLink": "Ligação simbólica" - }, - "scm": { - "amend": "Alterar", - "amendHeadCommit": "Compromisso HEAD", - "amendLastCommit": "Emendar o último compromisso", - "changeRepository": "Repositório de Mudanças...", - "config.untrackedChanges": "Controla o comportamento de mudanças não controladas.", - "config.untrackedChanges.hidden": "escondido", - "config.untrackedChanges.mixed": "misto", - "config.untrackedChanges.separate": "em separado", - "history": "História", - "noRepositoryFound": "Não foi encontrado nenhum repositório", - "unamend": "Unamend", - "unamendCommit": "Compromisso sem alterações" - }, - "search-in-workspace": { - "includeIgnoredFiles": "Incluir Ficheiros Ignorados", - "noFolderSpecified": "Não abriu nem especificou uma pasta. Apenas os ficheiros abertos são actualmente pesquisados.", - "resultSubset": "Este é apenas um subconjunto de todos os resultados. Use um termo de pesquisa mais específico para restringir a lista de resultados.", - "searchOnEditorModification": "Pesquisar o editor activo quando modificado." - }, - "secondary-window": { - "extract-widget": "Mover vista para a janela secundária" - }, - "shell-area": { - "secondary": "Janela Secundária", - "top": "Início" - }, - "task": { - "attachTask": "Anexar Tarefa...", - "clearHistory": "História clara", - "noTaskToRun": "Não foi encontrada nenhuma tarefa para executar. Configurar tarefas...", - "openUserTasks": "Tarefas de utilizador aberto" - }, - "terminal": { - "defaultProfile": "O perfil padrão utilizado em {0}", - "enableCopy": "Habilitar ctrl-c (cmd-c em macOS) para copiar o texto seleccionado", - "enablePaste": "Habilitar ctrl-v (cmd-v em macOS) para colar a partir da prancheta", - "profileArgs": "Os argumentos de concha que este perfil utiliza.", - "profileColor": "Um ID de cor de tema terminal a associar ao terminal.", - "profileDefault": "Escolher Perfil por Defeito...", - "profileIcon": "Um código de identificação para associar com o ícone do terminal.\nterminal-tmux: \"$(terminal-tmux)\".", - "profileNew": "Novo Terminal (Com Perfil)...", - "profilePath": "O caminho da concha que este perfil utiliza.", - "profiles": "Os perfis a apresentar aquando da criação de um novo terminal. Definir manualmente a propriedade do caminho com args opcionais.\nDefinir um perfil existente para `nulo` para ocultar o perfil da lista, por exemplo: `\"{0}\": nulo`.", - "rendererType": "Controla a forma como o terminal é renderizado.", - "selectProfile": "Seleccionar um perfil para o novo terminal", - "shell.deprecated": "Isto é depreciado, a nova forma recomendada para configurar a sua shell padrão é criando um perfil de terminal em 'terminal.integrated.profiles.{0}' e definindo o seu nome de perfil como o padrão em 'terminal.integrated.defaultProfile'.{0}.", - "shellArgsLinux": "Os argumentos de linha de comando a utilizar quando no terminal Linux.", - "shellArgsOsx": "Os argumentos de linha de comando a utilizar quando no terminal macOS.", - "shellArgsWindows": "Os argumentos de linha de comando a utilizar quando no terminal do Windows.", - "shellLinux": "O caminho da shell que o terminal utiliza no Linux (predefinição: '{0}'}).", - "shellOsx": "O caminho da concha que o terminal utiliza em macOS (predefinição: '{0}'}).", - "shellWindows": "O caminho da concha que o terminal utiliza no Windows. (predefinição: '{0}')." - }, - "test": { - "cancelAllTestRuns": "Cancelar todas as execuções de teste", - "testRunDefaultName": "{0} correr {1}", - "testRuns": "Testes" - }, - "toolbar": { - "addCommand": "Adicionar Comando à Barra de Ferramentas", - "addCommandPlaceholder": "Encontrar um comando para adicionar à barra de ferramentas", - "centerColumn": "Coluna Central", - "failedUpdate": "Falha em actualizar o valor de '{0}' em '{1}'.", - "filterIcons": "Ícones de filtro", - "iconSelectDialog": "Seleccione um Ícone para '{0}'.", - "iconSet": "Conjunto de Ícones", - "insertGroupLeft": "Inserir separador de grupo (Esquerda)", - "insertGroupRight": "Inserir separador de grupo (à direita)", - "leftColumn": "Coluna da Esquerda", - "openJSON": "Personalizar a barra de ferramentas (Abrir JSON)", - "removeCommand": "Remover Comando da Barra de Ferramentas", - "restoreDefaults": "Restaurar Padrões da Barra de Ferramentas", - "rightColumn": "Coluna da direita", - "selectIcon": "Seleccionar Ícone", - "toggleToolbar": "Barra de ferramentas Toggle", - "toolbarLocationPlaceholder": "Onde gostaria que o comando fosse adicionado?", - "useDefaultIcon": "Usar Ícone por defeito" - }, - "typehierarchy": { - "subtypeHierarchy": "Subtipo Hierarquia", - "supertypeHierarchy": "Hierarquia de Supertipo" - }, - "vsx-registry": { - "downloadCount": "Contagem de downloads: {0}", - "errorFetching": "Extensões de erro de busca.", - "errorFetchingConfigurationHint": "Isto pode ser causado por problemas de configuração da rede.", - "failedInstallingVSIX": "Falha na instalação {0} da VSIX.", - "invalidVSIX": "O ficheiro seleccionado não é um plugin \"*.vsix\" válido.", - "license": "Licença: {0}", - "recommendedExtensions": "Uma lista dos nomes das extensões recomendadas para utilização neste espaço de trabalho.", - "searchPlaceholder": "Pesquisar extensões em {0}", - "showInstalled": "Mostrar extensões instaladas", - "showRecommendedExtensions": "Controla se as notificações são mostradas para recomendações de extensão.", - "vsx-extensions-contribution": { - "update-version-uninstall-error": "Erro ao retirar a extensão: {0}.", - "update-version-version-error": "Falha na instalação da versão {0} de {1}." - } - }, - "webview": { - "goToReadme": "Ir para LEIAME", - "messageWarning": " O padrão de alojamento do terminal {0} foi alterado para `{1}`; alterar o padrão pode levar a vulnerabilidades de segurança. Ver `{2}` para mais informações." - }, - "workspace": { - "compareWithEachOther": "Comparar uns com os outros", - "confirmDeletePermanently.description": "Falha em apagar \"{0}\" utilizando o Lixo. Pretende, em vez disso, apagar permanentemente?", - "confirmDeletePermanently.solution": "Pode desactivar a utilização de Lixo nas preferências.", - "confirmDeletePermanently.title": "Erro ao apagar ficheiro", - "confirmMessage.delete": "Quer mesmo apagar os seguintes ficheiros?", - "confirmMessage.dirtyMultiple": "Quer mesmo apagar ficheiros {0} com alterações não guardadas?", - "confirmMessage.dirtySingle": "Quer mesmo eliminar {0} com alterações não guardadas?", - "confirmMessage.uriMultiple": "Quer realmente apagar todos os ficheiros {0} seleccionados?", - "confirmMessage.uriSingle": "Quer mesmo apagar {0}?", - "duplicate": "Duplicado", - "failApply": "Não foi possível aplicar alterações ao novo ficheiro", - "failSaveAs": "Não é possível executar \"{0}\" para o widget actual.", - "newFilePlaceholder": "Nome do ficheiro", - "newFolderPlaceholder": "Nome da pasta", - "noErasure": "Nota: Nada será apagado do disco", - "openRecentPlaceholder": "Digite o nome do espaço de trabalho que pretende abrir", - "openRecentWorkspace": "Espaço de Trabalho Recente Aberto...", - "preserveWindow": "Activar a abertura de espaços de trabalho na janela actual.", - "removeFolder": "Tem a certeza de que quer remover a seguinte pasta do espaço de trabalho?", - "removeFolders": "Tem a certeza de que quer remover as seguintes pastas do espaço de trabalho?", - "trashTitle": "Mover {0} para o Lixo", - "trustEmptyWindow": "Controla se o espaço de trabalho vazio é ou não de confiança por defeito.", - "trustEnabled": "Controla se a confiança no espaço de trabalho está ou não activada. Se estiver desactivado, todos os espaços de trabalho são de confiança.", - "trustRequest": "Uma extensão pede confiança no espaço de trabalho, mas a API correspondente ainda não é totalmente suportada. Quer confiar neste espaço de trabalho?", - "untitled-cleanup": "Parece haver muitos ficheiros de espaço de trabalho sem título. Por favor verifique {0} e remova quaisquer ficheiros não utilizados.", - "workspaceFolderAdded": "Foi criado um espaço de trabalho com múltiplas raízes. Quer guardar a configuração do seu espaço de trabalho como um ficheiro?", - "workspaceFolderAddedTitle": "Pasta adicionada ao espaço de trabalho" - } - } -} diff --git a/packages/core/i18n/nls.ru.json b/packages/core/i18n/nls.ru.json index 074a5c34cb027..26efaa004ac7e 100644 --- a/packages/core/i18n/nls.ru.json +++ b/packages/core/i18n/nls.ru.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "Откройте представление конфигурации AI", + "aiHistory:open": "Откройте представление истории ИИ", "debug.breakpoint.editCondition": "Изменить состояние...", + "notebook.cell.changeToCode": "Измените ячейку на код", + "notebook.cell.changeToMarkdown": "Измените ячейку на Мардаун", + "notebook.cell.insertMarkdownCellAbove": "Вставить ячейку для уценки выше", + "notebook.cell.insertMarkdownCellBelow": "Вставить ячейку для уценки ниже", "terminal:new:profile": "Создание нового интегрированного терминала из профиля", "terminal:profile:default": "Выберите профиль терминала по умолчанию", "theia": { @@ -7,6 +13,33 @@ "noCallers": "Вызывающих не обнаружено.", "open": "Иерархия открытых звонков" }, + "collaboration": { + "collaborate": "Сотрудничайте", + "collaboration": "Сотрудничество", + "collaborationWorkspace": "Рабочее пространство для совместной работы", + "connected": "Подключено", + "connectedSession": "Подключение к сеансу совместной работы", + "copiedInvitation": "Код приглашения скопирован в буфер обмена.", + "copyAgain": "Повторная копия", + "createRoom": "Создать новый сеанс совместной работы", + "creatingRoom": "Создание сессии", + "end": "Завершение сеанса совместной работы", + "endDetail": "Прервите сеанс, прекратите обмен содержимым и отмените доступ для других.", + "enterCode": "Введите код сеанса совместной работы", + "failedCreate": "Не удалось создать комнату: {0}", + "failedJoin": "Не удалось присоединиться к комнате: {0}", + "invite": "Пригласите других", + "inviteDetail": "Скопируйте код приглашения, чтобы поделиться им с другими людьми и присоединиться к сеансу.", + "joinRoom": "Присоединяйтесь к сессии совместной работы", + "joiningRoom": "Присоединение к сессии", + "leave": "Оставьте сессию совместной работы", + "leaveDetail": "Отключитесь от текущего сеанса совместной работы и закройте рабочую область.", + "selectCollaboration": "Выберите вариант сотрудничества", + "sharedSession": "Совместная сессия сотрудничества", + "startSession": "Начать или присоединиться к сеансу совместной работы", + "userWantsToJoin": "Пользователь '{0}' хочет присоединиться к комнате для совместной работы", + "whatToDo": "Что бы вы хотели сделать с другими соавторами?" + }, "core": { "about": { "compatibility": "{0} Совместимость", @@ -65,6 +98,13 @@ "next": "Далее (вниз)", "previous": "Предыдущий (Вверх)" }, + "secondaryWindow": { + "alwaysOnTop": "Если эта функция включена, вторичное окно остается выше всех остальных окон, в том числе окон различных приложений.", + "description": "Устанавливает начальное положение и размер извлеченного вторичного окна.", + "fullSize": "Положение и размер извлеченного виджета будут такими же, как и в запущенном приложении Theia.", + "halfWidth": "Положение и размер извлеченного виджета будут равны половине ширины запущенного приложения Theia.", + "originalSize": "Положение и размер извлеченного виджета будут такими же, как у исходного." + }, "silentNotifications": "Управляет тем, следует ли подавлять всплывающие окна уведомлений.", "tabDefaultSize": "Определяет размер по умолчанию для вкладок.", "tabMaximize": "Управляет тем, следует ли максимизировать вкладки при двойном щелчке.", @@ -94,14 +134,32 @@ "toggleTracing": "Включение/выключение трассировки связи с отладочными адаптерами" }, "editor": { + "diffEditor.wordWrap2": "Строки будут обернуты в соответствии с настройкой `#editor.wordWrap#`.", "dirtyEncoding": "Файл загрязнен. Пожалуйста, сначала сохраните его, а затем откройте с другой кодировкой.", - "editor.codeActionWidget.showHeaders": "Включить/выключить показ заголовков групп в меню действий кода.", - "editor.experimental.pasteActions.enabled": "Включение/выключение запуска правок из расширений при вставке.", + "editor.accessibilitySupport0": "Используйте API-интерфейсы платформы для обнаружения подключенного устройства чтения с экрана", + "editor.accessibilitySupport1": "Оптимизация для использования с устройством чтения с экрана", + "editor.accessibilitySupport2": "Предположим, что устройство чтения с экрана не подключено", + "editor.bracketPairColorization.enabled": "Управляет тем, включена или нет раскраска пар скобок. Используйте `#workbench.colorCustomizations#`, чтобы переопределить цвета выделения скобок.", + "editor.codeActionWidget.includeNearbyQuickfixes": "Включить/выключить показ ближайшего быстрого исправления в линии, если в данный момент нет диагностики.", + "editor.cursorSurroundingLinesStyle": "Контролирует, когда `#cursorSurroundingLines#` должен быть применен.", + "editor.detectIndentation": "Определяет, будут ли `#editor.tabSize#` и `#editor.insertSpaces#` автоматически определяться при открытии файла на основе его содержимого.", + "editor.dropIntoEditor.enabled": "Позволяет перетащить файл в текстовый редактор, удерживая клавишу `shift` (вместо открытия файла в редакторе).", "editor.formatOnSaveMode.modificationsIfAvailable": "Попытается отформатировать только модификации (требуется контроль исходного текста). Если контроль источника не может быть использован, то будет отформатирован весь файл.", + "editor.hover.hidingDelay": "Управляет задержкой в миллисекундах, после которой наведение будет скрыто. Требуется, чтобы `editor.hover.sticky` был включен.", "editor.inlayHints.enabled1": "Подсказки инкрустации отображаются по умолчанию и скрываются при нажатии `Ctrl+Alt`.", "editor.inlayHints.enabled2": "Подсказки инкрустации скрыты по умолчанию и отображаются при нажатии `Ctrl+Alt`.", + "editor.inlayHints.fontFamily": "Управляет семейством шрифтов подсказок инлея в редакторе. Если установлено значение empty, используется `#editor.fontFamily#`.", + "editor.inlayHints.fontSize": "Управляет размером шрифта подсказок инлея в редакторе. По умолчанию используется `#editor.fontSize#`, если настроенное значение меньше `5` или больше размера шрифта редактора.", + "editor.insertSpaces": "Вставлять пробелы при нажатии клавиши `Tab`. Эта настройка переопределяется в зависимости от содержимого файла, если включена опция `#editor.detectIndentation#`.", + "editor.occurrencesHighlight": "Управляет тем, должен ли редактор выделять вхождения семантических символов.", "editor.quickSuggestions": "Управляет тем, должны ли предложения автоматически появляться при вводе текста. Этим можно управлять при вводе комментариев, строк и другого кода. Быстрые предложения могут быть настроены на отображение в виде призрачного текста или виджета предложений. Также обратите внимание на настройку '#editor.suggestOnTriggerCharacters#', которая определяет, будут ли появляться предложения при использовании специальных символов.", - "editor.suggest.matchOnWordStartOnly": "При включении фильтрации IntelliSense требуется, чтобы первый символ совпадал с началом слова, например, `c` в `Console` или `WebContext`, но _нет_ в `description`. При отключении IntelliSense показывает больше результатов, но все равно сортирует их по качеству совпадения.", + "editor.stickyScroll.scrollWithEditor": "Включите прокрутку виджета липкой прокрутки с помощью горизонтальной полосы прокрутки редактора.", + "editor.suggestFontSize": "Размер шрифта для виджета предложения. Если установлено значение `0`, используется значение `#editor.fontSize#`.", + "editor.suggestLineHeight": "Высота строки для виджета предложения. Если установлено значение `0`, используется значение `#editor.lineHeight#`. Минимальное значение - 8.", + "editor.tabSize": "Количество пробелов, которым равна табуляция. Эта настройка переопределяется в зависимости от содержимого файла, если включена функция `#editor.detectIndentation#`.", + "editor.useTabStops": "Вставка и удаление пробельных символов следует за остановками табуляции.", + "editor.wordBasedSuggestions": "Указывает, следует ли вычислять завершения на основе слов в документе.", + "editor.wordBasedSuggestionsMode": "Служит для управления тем, на основе каких документов вычисляются словосочетания.", "files.autoSave": "Управляет [автосохранением](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) редакторов, в которых есть несохраненные изменения.", "files.autoSave.afterDelay": "Редактор с изменениями автоматически сохраняется по истечении настроенной `#files.autoSaveDelay#`.", "files.autoSave.off": "Редактор с изменениями никогда не сохраняется автоматически.", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "несколько секунд назад", "addSignedOff": "Добавить подписанный", - "amendReuseMessag": "Чтобы повторно использовать последнее сообщение фиксации, нажмите 'Enter' или 'Escape' для отмены.", + "amendReuseMessage": "Чтобы повторно использовать последнее сообщение фиксации, нажмите 'Enter' или 'Escape' для отмены.", "amendRewrite": "Переписать предыдущее сообщение о фиксации. Нажмите 'Enter' для подтверждения или 'Escape' для отмены.", "checkoutCreateLocalBranchWithName": "Создайте новый локальный филиал с именем: {0}. Нажмите 'Enter' для подтверждения или 'Escape' для отмены.", "checkoutProvideBranchName": "Пожалуйста, укажите название филиала.", @@ -333,6 +391,9 @@ "alreadyRunning": "Хостируемый экземпляр уже запущен.", "debugInstance": "Отладочный экземпляр", "debugMode": "Использование inspect или inspect-brk для отладки Node.js", + "debugPorts": { + "debugPort": "Порт, используемый для отладки Node.js на этом сервере" + }, "devHost": "Ведущий разработки", "failed": "Не удалось запустить размещенный экземпляр плагина: {0}", "hostedPlugin": "Хостируемый плагин", @@ -364,6 +425,10 @@ "webviewTrace": "Управляет трассировкой связи с веб-вью.", "webviewWarnIfUnsecure": "Предупреждает пользователей о том, что веб-просмотры в настоящее время развернуты небезопасно." }, + "preferences": { + "hostedPlugin": "Хостируемый плагин", + "toolbar": "Панель инструментов" + }, "preview": { "openByDefault": "По умолчанию вместо редактора открывается предварительный просмотр." }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "скрытый", "config.untrackedChanges.mixed": "смешанный", "config.untrackedChanges.separate": "отдельный", + "dirtyDiff": { + "close": "Закрыть Изменить Посмотреть" + }, "history": "История", "noRepositoryFound": "Репозиторий не найден", "unamend": "не изменять", @@ -401,8 +469,7 @@ "extract-widget": "Переместить вид в дополнительное окно" }, "shell-area": { - "secondary": "Вторичное окно", - "top": "Топ" + "secondary": "Вторичное окно" }, "task": { "attachTask": "Прикрепите задание...", @@ -422,6 +489,7 @@ "profilePath": "Путь к оболочке, которую использует данный профиль.", "profiles": "Профили, которые должны быть представлены при создании нового терминала. Задайте свойство path вручную с помощью необязательных args.\nУстановите для существующего профиля значение `null`, чтобы скрыть профиль из списка, например: `\"{0}\": null`.", "rendererType": "Управляет способом отображения терминала.", + "rendererTypeDeprecationMessage": "Тип рендерера больше не поддерживается как опция.", "selectProfile": "Выберите профиль для нового терминала", "shell.deprecated": "Это устарело, новым рекомендуемым способом настройки оболочки по умолчанию является создание профиля терминала в 'terminal.integrated.profiles.{0}' и установка имени профиля по умолчанию в 'terminal.integrated.defaultProfile.{0}.\".", "shellArgsLinux": "Аргументы командной строки для использования в терминале Linux.", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "Отмена всех тестовых запусков", + "stackFrameAt": "на", "testRunDefaultName": "{0} запустить {1}", "testRuns": "Тестовые испытания" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "Иерархия супертипов" }, "vsx-registry": { + "confirmDialogMessage": "Расширение \"{0}\" является непроверенным и может представлять угрозу безопасности.", + "confirmDialogTitle": "Вы уверены, что хотите продолжить установку?", "downloadCount": "Скачать граф: {0}", "errorFetching": "Ошибка при получении расширений.", "errorFetchingConfigurationHint": "Это может быть вызвано проблемами конфигурации сети.", "failedInstallingVSIX": "Не удалось установить {0} из VSIX.", "invalidVSIX": "Выбранный файл не является действительным плагином \"*.vsix\".", "license": "Лицензия: {0}", + "onlyShowVerifiedExtensionsDescription": "Это позволяет сайту {0} показывать только проверенные расширения.", + "onlyShowVerifiedExtensionsTitle": "Показывать только проверенные расширения", "recommendedExtensions": "Список имен расширений, рекомендуемых для использования в данной рабочей области.", "searchPlaceholder": "Поисковые расширения в {0}", "showInstalled": "Показать установленные расширения", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "Вы действительно хотите удалить все {0} выбранных файлов?", "confirmMessage.uriSingle": "Вы действительно хотите удалить {0}?", "duplicate": "Дубликат", - "failApply": "Не удалось применить изменения к новому файлу", "failSaveAs": "Невозможно выполнить \"{0}\" для текущего виджета.", "newFilePlaceholder": "Имя файла", "newFolderPlaceholder": "Имя папки", diff --git a/packages/core/i18n/nls.tr.json b/packages/core/i18n/nls.tr.json new file mode 100644 index 0000000000000..d855292211eb6 --- /dev/null +++ b/packages/core/i18n/nls.tr.json @@ -0,0 +1,585 @@ +{ + "aiConfiguration:open": "AI Yapılandırma görünümünü açın", + "aiHistory:open": "Yapay Zeka Geçmişi görünümünü açın", + "debug.breakpoint.editCondition": "Düzenleme Durumu...", + "notebook.cell.changeToCode": "Hücreyi Kod Olarak Değiştir", + "notebook.cell.changeToMarkdown": "Hücreyi Markdown Olarak Değiştirme", + "notebook.cell.insertMarkdownCellAbove": "Markdown Hücresini Yukarıya Ekleme", + "notebook.cell.insertMarkdownCellBelow": "Aşağıya Markdown Hücresi Ekleme", + "terminal:new:profile": "Profilden Yeni Entegre Terminal Oluşturma", + "terminal:profile:default": "Varsayılan Terminal Profilini seçin", + "theia": { + "callhierarchy": { + "noCallers": "Herhangi bir arayan tespit edilmedi.", + "open": "Açık Çağrı Hiyerarşisi" + }, + "collaboration": { + "collaborate": "İşbirliği yapın", + "collaboration": "İşbirliği", + "collaborationWorkspace": "İşbirliği Çalışma Alanı", + "connected": "Bağlı", + "connectedSession": "Bir işbirliği oturumuna bağlandı", + "copiedInvitation": "Davetiye kodu panoya kopyalandı.", + "copyAgain": "Tekrar Kopyala", + "createRoom": "Yeni İşbirliği Oturumu Oluştur", + "creatingRoom": "Oturum Oluşturma", + "end": "İşbirliği Oturumunu Sonlandırın", + "endDetail": "Oturumu sonlandırın, içerik paylaşımını durdurun ve başkalarının erişimini iptal edin.", + "enterCode": "İşbirliği oturum kodunu girin", + "failedCreate": "Oda oluşturulamadı: {0}", + "failedJoin": "Odaya katılamadı: {0}", + "invite": "Başkalarını Davet Edin", + "inviteDetail": "Oturuma katılmak üzere başkalarıyla paylaşmak için davet kodunu kopyalayın.", + "joinRoom": "İşbirliği Oturumuna Katılın", + "joiningRoom": "Katılma Oturumu", + "leave": "İşbirliği Oturumundan Ayrılın", + "leaveDetail": "Geçerli işbirliği oturumunun bağlantısını kesin ve çalışma alanını kapatın.", + "selectCollaboration": "İşbirliği seçeneğini belirleyin", + "sharedSession": "Bir işbirliği oturumu paylaştı", + "startSession": "İşbirliği oturumunu başlatın veya oturuma katılın", + "userWantsToJoin": "Kullanıcı '{0}' işbirliği odasına katılmak istiyor", + "whatToDo": "Diğer işbirlikçilerle neler yapmak istersiniz?" + }, + "core": { + "about": { + "compatibility": "{0} Uyumluluk", + "defaultApi": "Varsayılan {0} API", + "version": "Versiyon" + }, + "common": { + "closeAll": "Tüm Sekmeleri Kapat", + "closeAllTabMain": "Ana Alandaki Tüm Sekmeleri Kapat", + "closeOtherTabMain": "Ana Alandaki Diğer Sekmeleri Kapat", + "closeOthers": "Diğer Sekmeleri Kapat", + "closeRight": "Sekmeleri Sağa Kapat", + "closeTab": "Sekmeyi Kapat", + "closeTabMain": "Ana Alandaki Sekmeyi Kapat", + "collapseAllTabs": "Tüm Yan Panelleri Daralt", + "collapseBottomPanel": "Alt Paneli Değiştir", + "collapseTab": "Yan Paneli Daralt", + "showNextTabGroup": "Sonraki Sekme Grubuna Geç", + "showNextTabInGroup": "Gruptaki Sonraki Sekmeye Geç", + "showPreviousTabGroup": "Önceki Sekme Grubuna Geç", + "showPreviousTabInGroup": "Grupta Önceki Sekmeye Geç", + "toggleMaximized": "Maksimize Edilmiş Değiştir" + }, + "copyInfo": "Yolunu kopyalamak için önce bir dosya açın", + "copyWarn": "Lütfen tarayıcının kopyala komutunu veya kısayolunu kullanın.", + "cutWarn": "Lütfen tarayıcının kes komutunu veya kısayolunu kullanın.", + "enhancedPreview": { + "classic": "Temel bilgileri içeren sekmenin basit bir önizlemesini görüntüleyin.", + "enhanced": "Sekmenin ek bilgiler içeren gelişmiş bir önizlemesini görüntüleyin.", + "visual": "Sekmenin görsel bir önizlemesini görüntüleyin." + }, + "file": { + "browse": "Gözat" + }, + "highlightModifiedTabs": "Değiştirilmiş (kirli) düzenleyici sekmelerinde bir üst kenarlık çizilip çizilmeyeceğini kontrol eder.", + "keybindingStatus": "{0} basıldı, daha fazla tuş beklendi", + "keyboard": { + "choose": "Klavye Düzenini Seçin", + "chooseLayout": "Bir klavye düzeni seçin", + "current": "(güncel: {0})", + "currentLayout": " - mevcut düzen", + "mac": "Mac Klavyeleri", + "pc": "PC Klavyeleri", + "tryDetect": "Tarayıcı bilgilerinden ve basılan tuşlardan klavye düzenini tespit etmeye çalışın." + }, + "navigator": { + "clipboardWarn": "Panoya erişim reddedildi. Tarayıcınızın izinlerini kontrol edin.", + "clipboardWarnFirefox": "Pano API'si mevcut değildir. '{1}' sayfasındaki '{0}' tercihi ile etkinleştirilebilir. Ardından Theia'yı yeniden yükleyin. FireFox'un sistem panosuna tam erişim sağlamasına izin vereceğini unutmayın." + }, + "offline": "Çevrimdışı", + "pasteWarn": "Lütfen tarayıcının yapıştırma komutunu veya kısayolunu kullanın.", + "quitMessage": "Kaydedilmemiş hiçbir değişiklik kaydedilmeyecektir.", + "resetWorkbenchLayout": "Çalışma Tezgahı Düzenini Sıfırla", + "searchbox": { + "close": "Kapat (Kaçış)", + "next": "Sonraki (Aşağı)", + "previous": "Önceki (Yukarı)" + }, + "secondaryWindow": { + "alwaysOnTop": "Etkinleştirildiğinde, ikincil pencere farklı uygulamalarınkiler de dahil olmak üzere diğer tüm pencerelerin üzerinde kalır.", + "description": "Çıkarılan ikincil pencerenin ilk konumunu ve boyutunu ayarlar.", + "fullSize": "Çıkarılan widget'ın konumu ve boyutu, çalışan Theia uygulamasıyla aynı olacaktır.", + "halfWidth": "Çıkarılan widget'ın konumu ve boyutu, çalışan Theia uygulamasının genişliğinin yarısı kadar olacaktır.", + "originalSize": "Çıkarılan widget'ın konumu ve boyutu orijinal widget ile aynı olacaktır." + }, + "silentNotifications": "Bildirim açılır pencerelerinin bastırılıp bastırılmayacağını denetler.", + "tabDefaultSize": "Sekmeler için varsayılan boyutu belirtir.", + "tabMaximize": "Çift tıklamada sekmelerin büyütülüp büyütülmeyeceğini kontrol eder.", + "tabMinimumSize": "Sekmeler için minimum boyutu belirtir.", + "tabShrinkToFit": "Sekmeleri mevcut alana sığacak şekilde küçültün." + }, + "debug": { + "addConfigurationPlaceholder": "Yapılandırma eklemek için çalışma alanı kökünü seçin", + "breakpoint": "kesme noktası", + "compound-cycle": "Başlat yapılandırması '{0}' kendisiyle birlikte bir döngü içerir", + "continueAll": "Tümü Devam Ediyor", + "copyExpressionValue": "İfade Değerini Kopyala", + "dataBreakpoint": "veri kesme noktası", + "debugVariableInput": "{0} Değerini Ayarla", + "entry": "Giriş", + "exception": "istisna", + "functionBreakpoint": "fonksiyon kesme noktası", + "goto": "Goto", + "instruction-breakpoint": "Talimat Kesme Noktası", + "instructionBreakpoint": "talimat kesme noktası", + "missingConfiguration": "Dinamik yapılandırma '{0}:{1}' eksik veya uygulanabilir değil", + "pause": "duraklama", + "pauseAll": "Tümünü Duraklat", + "reveal": "Açığa Çıkar", + "step": "adım", + "threads": "İplikler", + "toggleTracing": "Hata ayıklama bağdaştırıcılarıyla izleme iletişimini etkinleştirme/devre dışı bırakma" + }, + "editor": { + "diffEditor.wordWrap2": "Satırlar `#editor.wordWrap#` ayarına göre sarılacaktır.", + "dirtyEncoding": "Dosya kirli. Lütfen başka bir kodlama ile yeniden açmadan önce kaydedin.", + "editor.accessibilitySupport0": "Bir Ekran Okuyucunun takılı olduğunu algılamak için platform API'lerini kullanma", + "editor.accessibilitySupport1": "Ekran Okuyucu ile kullanım için optimize edin", + "editor.accessibilitySupport2": "Bir ekran okuyucunun bağlı olmadığını varsayalım", + "editor.bracketPairColorization.enabled": "Ayraç çifti renklendirmesinin etkin olup olmadığını kontrol eder. Ayraç vurgu renklerini geçersiz kılmak için `#workbench.colorCustomizations#` kullanın.", + "editor.codeActionWidget.includeNearbyQuickfixes": "O anda bir tanılama üzerinde değilken bir hat içindeki en yakın hızlı düzeltmeyi göstermeyi etkinleştirin/devre dışı bırakın.", + "editor.cursorSurroundingLinesStyle": "Ne zaman `#cursorSurroundingLines#` uygulanacağını kontrol eder.", + "editor.detectIndentation": "Dosya içeriğine bağlı olarak bir dosya açıldığında `#editor.tabSize#` ve `#editor.insertSpaces#` öğelerinin otomatik olarak algılanıp algılanmayacağını kontrol eder.", + "editor.dropIntoEditor.enabled": "Bir dosyayı `shift` tuşunu basılı tutarak bir metin düzenleyicisine sürükleyip bırakıp bırakamayacağınızı kontrol eder (dosyayı bir düzenleyicide açmak yerine).", + "editor.formatOnSaveMode.modificationsIfAvailable": "Yalnızca değişiklikleri biçimlendirmeye çalışır (kaynak kontrolü gerektirir). Kaynak kontrolü kullanılamazsa, tüm dosya biçimlendirilecektir.", + "editor.hover.hidingDelay": "Üzerine gelinen öğenin gizlenmesinden sonraki gecikmeyi milisaniye cinsinden kontrol eder. Etkinleştirilmek için `editor.hover.sticky` gerektirir.", + "editor.inlayHints.enabled1": "Kakma ipuçları varsayılan olarak gösterilir ve Ctrl+Alt tuşları basılı tutulduğunda gizlenir", + "editor.inlayHints.enabled2": "Kakma ipuçları varsayılan olarak gizlidir ve Ctrl+Alt tuşları basılı tutulduğunda gösterilir", + "editor.inlayHints.fontFamily": "Düzenleyicideki yerleşik ipuçlarının yazı tipi ailesini kontrol eder. Boş olarak ayarlandığında, `#editor.fontFamily#` kullanılır.", + "editor.inlayHints.fontSize": "Düzenleyicideki yerleşik ipuçlarının yazı tipi boyutunu kontrol eder. Yapılandırılan değer `5`ten küçük veya düzenleyici yazı tipi boyutundan büyük olduğunda varsayılan olarak `#editor.fontSize#` kullanılır.", + "editor.insertSpaces": "Tab` tuşuna basıldığında boşluk ekler. Bu ayar, `#editor.detectIndentation#` açık olduğunda dosya içeriğine göre geçersiz kılınır.", + "editor.occurrencesHighlight": "Düzenleyicinin anlamsal sembol oluşumlarını vurgulayıp vurgulamayacağını kontrol eder.", + "editor.quickSuggestions": "Yazarken önerilerin otomatik olarak gösterilip gösterilmeyeceğini kontrol eder. Bu, yorumlar, dizeler ve diğer kodların yazılması için kontrol edilebilir. Hızlı öneri, hayalet metin olarak veya öneri widget'ı ile gösterilecek şekilde yapılandırılabilir. Ayrıca önerilerin özel karakterler tarafından tetiklenip tetiklenmeyeceğini kontrol eden '#editor.suggestOnTriggerCharacters#'ayarına da dikkat edin.", + "editor.stickyScroll.scrollWithEditor": "Yapışkan kaydırma widget'ının düzenleyicinin yatay kaydırma çubuğu ile kaydırılmasını etkinleştirin.", + "editor.suggestFontSize": "Suggest widget'ı için yazı tipi boyutu. 0` olarak ayarlandığında, `#editor.fontSize#` değeri kullanılır.", + "editor.suggestLineHeight": "Suggest widget'ı için satır yüksekliği. 0` olarak ayarlandığında, `#editor.lineHeight#` değeri kullanılır. Minimum değer 8`dir.", + "editor.tabSize": "Bir sekmenin eşit olduğu boşluk sayısı. Bu ayar, `#editor.detectIndentation#` açık olduğunda dosya içeriğine göre geçersiz kılınır.", + "editor.useTabStops": "Boşluk ekleme ve silme sekme duraklarını takip eder.", + "editor.wordBasedSuggestions": "Tamamlamaların belgedeki sözcüklere göre hesaplanıp hesaplanmayacağını denetler.", + "editor.wordBasedSuggestionsMode": "Kelime tabanlı tamamlamaların hangi belgelerden hesaplanacağını kontrol eder.", + "files.autoSave": "Kaydedilmemiş değişiklikleri olan editörlerin [otomatik kaydet] (https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) kontrollerini yapar.", + "files.autoSave.afterDelay": "Değişiklikler içeren bir düzenleyici, yapılandırılan `#files.autoSaveDelay#` değerinden sonra otomatik olarak kaydedilir.", + "files.autoSave.off": "Değişiklik yapılan bir düzenleyici asla otomatik olarak kaydedilmez.", + "files.autoSave.onFocusChange": "Düzenleyici odağı kaybettiğinde değişiklik yapılan bir düzenleyici otomatik olarak kaydedilir.", + "files.autoSave.onWindowChange": "Değişiklikler içeren bir düzenleyici, pencere odağını kaybettiğinde otomatik olarak kaydedilir.", + "formatOnSaveTimeout": "Dosya kaydedildiğinde çalıştırılan biçimlendirmenin iptal edileceği milisaniye cinsinden zaman aşımı.", + "persistClosedEditors": "Pencere yeniden yüklenirken çalışma alanı için kapalı düzenleyici geçmişinin devam ettirilip ettirilmeyeceğini kontrol eder.", + "showAllEditors": "Tüm Açık Editörleri Göster", + "splitHorizontal": "Bölme Düzenleyici Yatay", + "splitVertical": "Bölünmüş Editör Dikey", + "toggleStickyScroll": "Yapışkan Kaydırmayı Değiştir" + }, + "file-search": { + "toggleIgnoredFiles": " (Yok sayılan dosyaları göstermek/gizlemek için {0} adresine basın)" + }, + "fileDialog": { + "showHidden": "Gizli dosyaları göster" + }, + "fileSystem": { + "fileResource": { + "overWriteBody": "Dosya sisteminde '{0}' adresinde yapılan değişikliklerin üzerine yazmak istiyor musunuz?" + } + }, + "filesystem": { + "copiedToClipboard": "İndirme bağlantısını panoya kopyaladım.", + "copyDownloadLink": "İndirme Bağlantısını Kopyala", + "dialog": { + "initialLocation": "İlk Konuma Git", + "multipleItemMessage": "Yalnızca bir öğe seçebilirsiniz", + "name": "İsim:", + "navigateBack": "Geri Git", + "navigateForward": "İleriye Yönelin", + "navigateUp": "Bir Dizinde Gezinme" + }, + "fileResource": { + "binaryFileQuery": "Açmak biraz zaman alabilir ve IDE'nin yanıt vermemesine neden olabilir. Yine de '{0}' adresini açmak istiyor musunuz?", + "binaryTitle": "Dosya ya ikilidir ya da desteklenmeyen bir metin kodlaması kullanmaktadır.", + "largeFileTitle": "Dosya çok büyük ({0}).", + "overwriteTitle": "Dosya sisteminde '{0}' dosyası değiştirildi." + }, + "filesExclude": "Dosya ve klasörleri dışlamak için glob kalıplarını yapılandırın. Örneğin, dosya Gezgini bu ayara göre hangi dosya ve klasörlerin gösterileceğine veya gizleneceğine karar verir.", + "format": "Format:", + "maxConcurrentUploads": "Birden fazla dosya yüklerken yüklenecek maksimum eşzamanlı dosya sayısı. 0, tüm dosyaların eşzamanlı olarak yükleneceği anlamına gelir.", + "maxFileSizeMB": "Açılabilecek maksimum dosya boyutunu MB cinsinden kontrol eder.", + "prepareDownload": "İndirme hazırlanıyor...", + "prepareDownloadLink": "İndirme bağlantısı hazırlanıyor...", + "processedOutOf": "İşlenmiş {0} dışında {1}", + "replaceTitle": "Dosya Değiştir", + "uploadFiles": "Dosya Yükle...", + "uploadedOutOf": "{0} adresinden yüklendi {1}" + }, + "getting-started": { + "apiComparator": "{0} API Uyumluluğu", + "newExtension": "Yeni Bir Uzantı İnşa Etmek", + "newPlugin": "Yeni Bir Eklenti Oluşturma", + "startup-editor": { + "welcomePage": "{0} ve uzantılarını kullanmaya başlamanıza yardımcı olacak içeriğe sahip Hoş Geldiniz sayfasını açın." + } + }, + "git": { + "aFewSecondsAgo": "birkaç saniye önce", + "addSignedOff": "Signed-off-by ekleyin", + "amendReuseMessage": "Son taahhüt mesajını yeniden kullanmak için 'Enter' tuşuna veya iptal etmek için 'Escape' tuşuna basın.", + "amendRewrite": "Önceki commit mesajını yeniden yazın. Onaylamak için 'Enter' tuşuna veya iptal etmek için 'Escape' tuşuna basın.", + "checkoutCreateLocalBranchWithName": "Adı: {0} olan yeni bir yerel şube oluşturun. Onaylamak için 'Enter' tuşuna veya iptal etmek için 'Escape' tuşuna basın.", + "checkoutProvideBranchName": "Lütfen bir şube adı belirtiniz. ", + "checkoutSelectRef": "Ödeme yapmak veya yeni bir yerel şube oluşturmak için bir ref seçin:", + "cloneQuickInputLabel": "Lütfen bir Git deposu konumu girin. Onaylamak için 'Enter' tuşuna veya iptal etmek için 'Escape' tuşuna basın.", + "cloneRepository": "Git deposunu klonlayın: {0}. Onaylamak için 'Enter' tuşuna veya iptal etmek için 'Escape' tuşuna basın.", + "compareWith": "İle Karşılaştırın...", + "compareWithBranchOrTag": "O anda etkin olan {0} dalı ile karşılaştırmak için bir dal veya etiket seçin:", + "diff": "Diff", + "dirtyDiffLinesLimit": "Düzenleyicinin satır sayısı bu sınırı aşarsa, kirli fark süslemelerini gösterme.", + "dropStashMessage": "Zula başarıyla kaldırıldı.", + "editorDecorationsEnabled": "Düzenleyicide git süslemelerini göster.", + "fetchPickRemote": "Getirmek için bir uzaktan kumanda seçin:", + "gitDecorationsColors": "Navigatörde renk dekorasyonunu kullanın.", + "mergeQuickPickPlaceholder": "Şu anda etkin olan {0} dalıyla birleştirmek için bir dal seçin:", + "missingUserInfo": "git'te 'user.name' ve 'user.email' dosyalarınızı yapılandırdığınızdan emin olun.", + "noHistoryForError": "için mevcut bir geçmiş bulunmamaktadır. {0}", + "noPreviousCommit": "Değişiklik yapmak için önceden taahhüt yok", + "noRepositoriesSelected": "Hiçbir depo seçilmemiştir.", + "prepositionIn": "içinde", + "repositoryNotInitialized": "{0} deposu henüz başlatılmadı.", + "stashChanges": "Zula değişiklikleri. Onaylamak için 'Enter' tuşuna veya iptal etmek için 'Escape' tuşuna basın.", + "stashChangesWithMessage": "Zula mesajla değişir: {0}. Onaylamak için 'Enter' tuşuna veya iptal etmek için 'Escape' tuşuna basın.", + "tabTitleIndex": "{0} (Dizin)", + "tabTitleWorkingTree": "{0} (Çalışma ağacı)", + "toggleBlameAnnotations": "Suçlama Ek Açıklamalarını Değiştir" + }, + "keybinding-schema-updater": { + "deprecation": "Bunun yerine `when` cümlesini kullanın." + }, + "keymaps": { + "addKeybindingTitle": "Şunlar için Tuş Bağlama Ekle {0}", + "editKeybinding": "Tuş Bağlamasını Düzenle...", + "editKeybindingTitle": "için Tuş Bağlamasını Düzenle {0}", + "editWhenExpression": "İfade Ne Zaman Düzenlenir...", + "editWhenExpressionTitle": "İfade için Düzenle {0}", + "keybinding": { + "copy": "Tuş Bağlamayı Kopyala", + "copyCommandId": "Tuş Bağlama Komut Kimliğini Kopyala", + "copyCommandTitle": "Tuş Bağlantısını Kopyala Komut Başlığı", + "edit": "Tuş Bağlamasını Düzenle...", + "editWhenExpression": "İfade Olduğunda Tuş Bağlantısını Düzenle..." + }, + "keybindingCollidesValidation": "tuş bağlama şu anda çakışıyor", + "requiredKeybindingValidation": "tuş bağlama değeri gereklidir", + "resetKeybindingConfirmation": "Bu tuş bağlamayı gerçekten varsayılan değerine sıfırlamak istiyor musunuz?", + "resetKeybindingTitle": "için tuş bağlamayı sıfırla {0}", + "resetMultipleKeybindingsWarning": "Bu komut için birden fazla tuş ataması varsa, hepsi sıfırlanacaktır." + }, + "localize": { + "offlineTooltip": "Arka uca bağlanılamıyor." + }, + "markers": { + "clearAll": "Tümünü Temizle", + "noProblems": "Çalışma alanında şu ana kadar herhangi bir sorun tespit edilmedi.", + "tabbarDecorationsEnabled": "Sekme çubuklarında sorun dekoratörlerini (tanılama işaretleyicileri) gösterin." + }, + "memory-inspector": { + "addressTooltip": "Görüntülenecek bellek konumu, bir adres veya bir adrese göre değerlendirilen ifade", + "ascii": "ASCII", + "binary": "İkili", + "byteSize": "Bayt Boyutu", + "bytesPerGroup": "Grup Başına Bayt", + "closeSettings": "Ayarları Kapat", + "columns": "Sütunlar", + "command": { + "createNewMemory": "Yeni Bellek Denetçisi Oluşturun", + "createNewRegisterView": "Yeni Kayıt Görünümü Oluştur", + "followPointer": "Pointer'ı Takip Edin", + "followPointerMemory": "Bellek Denetçisinde İşaretçiyi Takip Etme", + "resetValue": "Sıfırlama Değeri", + "showRegister": "Bellek Denetçisinde Kaydı Göster", + "viewVariable": "Değişkeni Bellek Denetçisinde Göster" + }, + "data": "Veri", + "decimal": "Ondalık", + "diff": { + "label": "Diff: {0}" + }, + "diff-widget": { + "offset-label": "{0} Ofset", + "offset-title": "Belleğin kaydırılacağı baytlar {0}" + }, + "editable": { + "apply": "Değişiklikleri Uygula", + "clear": "Açık Değişiklikler" + }, + "endianness": "Endianness", + "extraColumn": "Ekstra Sütun", + "groupsPerRow": "Satır Başına Gruplar", + "hexadecimal": "Onaltılık", + "length": "Uzunluk", + "lengthTooltip": "Getirilecek bayt sayısı, ondalık veya onaltılık olarak", + "memory": { + "addressField": { + "memoryReadError": "Konum alanına bir adres veya ifade girin." + }, + "freeze": "Bellek Görünümünü Dondur", + "hideSettings": "Ayarlar Panelini Gizle", + "readError": { + "bounds": "Bellek sınırları aşıldı, sonuç kesilecektir.", + "noContents": "Şu anda kullanılabilir bellek içeriği yok." + }, + "readLength": { + "memoryReadError": "Uzunluk alanına bir uzunluk (ondalık veya onaltılık sayı) girin." + }, + "showSettings": "Ayarlar Panelini Göster", + "unfreeze": "Bellek Görünümünü Çöz", + "userError": "Bellek alınırken bir hata oluştu." + }, + "memoryCategory": "Bellek Denetçisi", + "memoryInspector": "Bellek Denetçisi", + "memoryTitle": "Hafıza", + "octal": "Sekizli", + "offset": "Ofset", + "offsetTooltip": "Gezinirken geçerli bellek konumuna eklenecek ofset", + "provider": { + "localsError": "Yerel değişkenler okunamıyor. Etkin hata ayıklama oturumu yok.", + "readError": "Bellek okunamıyor. Etkin hata ayıklama oturumu yok.", + "writeError": "Bellek yazılamıyor. Etkin hata ayıklama oturumu yok." + }, + "register": "Kayıt Olun", + "register-widget": { + "filter-placeholder": "Filtre (ile başlar)" + }, + "registerReadError": "Kayıtların alınmasında bir hata oluştu.", + "registers": "Kayıtlar", + "toggleComparisonWidgetVisibility": "Karşılaştırma Widget Görünürlüğünü Aç / Kapat", + "utils": { + "afterBytes": "Karşılaştırmak istediğiniz her iki widget'a da bellek yüklemeniz gerekir. {0} adresinde bellek yüklü değildir.", + "bytesMessage": "Karşılaştırmak istediğiniz her iki widget'a da bellek yüklemeniz gerekir. {0} adresinde bellek yüklü değildir." + } + }, + "messages": { + "notificationTimeout": "Bilgilendirici bildirimler bu zaman aşımından sonra gizlenecektir.", + "toggleNotifications": "Bildirimleri Aç / Kapat" + }, + "mini-browser": { + "typeUrl": "Bir URL yazın" + }, + "monaco": { + "noSymbolsMatching": "Eşleşen sembol yok", + "typeToSearchForSymbols": "Sembolleri aramak için yazın" + }, + "navigator": { + "autoReveal": "Otomatik Gösterge", + "clipboardWarn": "Panoya erişim reddedildi. Tarayıcınızın izinlerini kontrol edin.", + "clipboardWarnFirefox": "Pano API'si mevcut değildir. '{1}' sayfasındaki '{0}' tercihi ile etkinleştirilebilir. Ardından Theia'yı yeniden yükleyin. FireFox'un sistem panosuna tam erişim sağlamasına izin vereceğini unutmayın.", + "refresh": "Explorer'da Yenile", + "reveal": "Explorer'da Göster", + "toggleHiddenFiles": "Gizli Dosyaları Aç / Kapat" + }, + "output": { + "clearOutputChannel": "Çıkış Kanalını Temizle...", + "closeOutputChannel": "Çıkış Kanalını Kapat...", + "hiddenChannels": "Gizli Kanallar", + "hideOutputChannel": "Çıkış Kanalını Gizle...", + "maxChannelHistory": "Bir çıkış kanalındaki maksimum giriş sayısı.", + "outputChannels": "Çıkış Kanalları", + "showOutputChannel": "Çıkış Kanalını Göster..." + }, + "plugin": { + "blockNewTab": "Tarayıcınız yeni bir sekme açılmasını engelledi" + }, + "plugin-dev": { + "alreadyRunning": "Barındırılan örnek zaten çalışıyor.", + "debugInstance": "Hata Ayıklama Örneği", + "debugMode": "Node.js hata ayıklama için inspect veya inspect-brk kullanma", + "debugPorts": { + "debugPort": "Bu sunucunun Node.js hata ayıklaması için kullanılacak bağlantı noktası" + }, + "devHost": "Geliştirme Ev Sahibi", + "failed": "Barındırılan eklenti örneği çalıştırılamadı: {0}", + "hostedPlugin": "Barındırılan Eklenti", + "hostedPluginRunning": "Barındırılan Eklenti: Çalışıyor", + "hostedPluginStarting": "Barındırılan Eklenti: Başlıyor", + "hostedPluginStopped": "Barındırılan Eklenti: Durduruldu", + "hostedPluginWatching": "Barındırılan Eklenti: İzleme", + "instanceTerminated": "{0} sonlandırıldı", + "launchOutFiles": "Oluşturulan JavaScript dosyalarını bulmak için glob kalıpları dizisi (`${pluginPath}` eklentinin gerçek yolu ile değiştirilecektir).", + "noValidPlugin": "Belirtilen klasör geçerli bir eklenti içermiyor.", + "notRunning": "Barındırılan örnek çalışmıyor.", + "pluginFolder": "Eklenti klasörü olarak ayarlanmıştır: {0}", + "preventedNewTab": "Tarayıcınız yeni bir sekme açılmasını engelledi", + "restartInstance": "Örneği Yeniden Başlat", + "running": "Barındırılan örnek şu adreste çalışıyor:", + "select": "Seçiniz", + "selectPath": "Yol Seçin", + "startInstance": "Örneği Başlat", + "starting": "Barındırılan örnek sunucusu başlatılıyor ...", + "stopInstance": "Örneği Durdur", + "unknownTerminated": "Örnek sonlandırıldı", + "watchMode": "Geliştirme aşamasındaki eklenti üzerinde izleyiciyi çalıştırın" + }, + "plugin-ext": { + "authentication-main": { + "loginTitle": "Giriş" + }, + "plugins": "Eklentiler", + "webviewTrace": "Web görünümleri ile iletişim izlemeyi kontrol eder.", + "webviewWarnIfUnsecure": "Kullanıcıları web görünümlerinin şu anda güvenli olmayan bir şekilde dağıtıldığı konusunda uyarır." + }, + "preferences": { + "hostedPlugin": "Barındırılan Eklenti", + "toolbar": "Araç Çubuğu" + }, + "preview": { + "openByDefault": "Varsayılan olarak düzenleyici yerine önizlemeyi açın." + }, + "property-view": { + "created": "Oluşturuldu", + "directory": "Rehber", + "lastModified": "Son değişiklik", + "location": "Konum", + "noProperties": "Mevcut mülk yok.", + "properties": "Özellikler", + "size": "Boyut", + "symbolicLink": "Sembolik bağlantı" + }, + "scm": { + "amend": "Değiştirmek", + "amendHeadCommit": "HEAD Commit", + "amendLastCommit": "Son taahhüdü değiştirin", + "changeRepository": "Depoyu Değiştir...", + "config.untrackedChanges": "İzlenmeyen değişikliklerin nasıl davranacağını kontrol eder.", + "config.untrackedChanges.hidden": "gizli", + "config.untrackedChanges.mixed": "karışık", + "config.untrackedChanges.separate": "ayrı", + "dirtyDiff": { + "close": "Kapat Değişim Peek Görünümü" + }, + "history": "Tarih", + "noRepositoryFound": "Depo bulunamadı", + "unamend": "Unamend", + "unamendCommit": "Değişikliği kaldır" + }, + "search-in-workspace": { + "includeIgnoredFiles": "Yoksayılan Dosyaları Dahil Etme", + "noFolderSpecified": "Bir klasör açmadınız veya belirtmediniz. Şu anda yalnızca açık dosyalar aranmaktadır.", + "resultSubset": "Bu, tüm sonuçların yalnızca bir alt kümesidir. Sonuç listesini daraltmak için daha spesifik bir arama terimi kullanın.", + "searchOnEditorModification": "Değiştirildiğinde etkin düzenleyicide arama yapın." + }, + "secondary-window": { + "extract-widget": "Görünümü İkincil Pencereye Taşı" + }, + "shell-area": { + "secondary": "İkincil Pencere" + }, + "task": { + "attachTask": "Görev Ekle...", + "clearHistory": "Geçmişi Temizle", + "noTaskToRun": "Çalıştırılacak görev bulunamadı. Görevleri Yapılandır...", + "openUserTasks": "Kullanıcı Görevlerini Aç" + }, + "terminal": { + "defaultProfile": "üzerinde kullanılan varsayılan profil {0}", + "enableCopy": "Seçili metni kopyalamak için ctrl-c'yi (macOS'ta cmd-c) etkinleştirin", + "enablePaste": "Panodan yapıştırmak için ctrl-v'yi (macOS'ta cmd-v) etkinleştirin", + "profileArgs": "Bu profilin kullandığı kabuk argümanları.", + "profileColor": "Terminalle ilişkilendirilecek bir terminal tema rengi kimliği.", + "profileDefault": "Varsayılan Profil'i seçin...", + "profileIcon": "Terminal simgesiyle ilişkilendirilecek bir codicon kimliği.\nterminal-tmux:\"$(terminal-tmux)\"", + "profileNew": "Yeni Terminal (Profilli)...", + "profilePath": "Bu profilin kullandığı kabuğun yolu.", + "profiles": "Yeni bir terminal oluştururken sunulacak profiller. Yol özelliğini isteğe bağlı args ile manuel olarak ayarlayın.\nProfili listeden gizlemek için mevcut bir profili `null` olarak ayarlayın, örneğin: `\"{0}\": null`.", + "rendererType": "Terminalin nasıl oluşturulacağını kontrol eder.", + "rendererTypeDeprecationMessage": "Oluşturucu türü artık bir seçenek olarak desteklenmemektedir.", + "selectProfile": "Yeni terminal için bir profil seçin", + "shell.deprecated": "Bu kullanımdan kaldırılmıştır, varsayılan kabuğunuzu yapılandırmak için önerilen yeni yol 'terminal.integrated.profiles.{0}' içinde bir terminal profili oluşturmak ve profil adını 'terminal.integrated.defaultProfile.{0}' içinde varsayılan olarak ayarlamaktır.", + "shellArgsLinux": "Linux terminalindeyken kullanılacak komut satırı argümanları.", + "shellArgsOsx": "macOS terminalindeyken kullanılacak komut satırı argümanları.", + "shellArgsWindows": "Windows terminalindeyken kullanılacak komut satırı argümanları.", + "shellLinux": "Terminalin Linux üzerinde kullandığı kabuğun yolu (varsayılan: '{0}'}).", + "shellOsx": "Terminalin macOS üzerinde kullandığı kabuğun yolu (varsayılan: '{0}'}).", + "shellWindows": "Terminalin Windows üzerinde kullandığı kabuğun yolu. (varsayılan: '{0}')." + }, + "test": { + "cancelAllTestRuns": "Tüm Test Çalışmalarını İptal Et", + "stackFrameAt": "at", + "testRunDefaultName": "{0} koşmak {1}", + "testRuns": "Test Çalışmaları" + }, + "toolbar": { + "addCommand": "Araç Çubuğuna Komut Ekleme", + "addCommandPlaceholder": "Araç çubuğuna eklemek için bir komut bulun", + "centerColumn": "Orta Kolon", + "failedUpdate": "'{1}' içindeki '{0}' değeri güncellenemedi.", + "filterIcons": "Filtre Simgeleri", + "iconSelectDialog": "'{0}' için bir Simge seçin", + "iconSet": "Simge Seti", + "insertGroupLeft": "Grup Ayırıcı Ekle (Sol)", + "insertGroupRight": "Grup Ayırıcı Ekle (Sağ)", + "leftColumn": "Sol Sütun", + "openJSON": "Araç Çubuğunu Özelleştir (JSON'u Aç)", + "removeCommand": "Command'ı Araç Çubuğundan Kaldır", + "restoreDefaults": "Araç Çubuğu Varsayılanlarını Geri Yükle", + "rightColumn": "Sağ Sütun", + "selectIcon": "Simge Seçin", + "toggleToolbar": "Araç Çubuğunu Değiştir", + "toolbarLocationPlaceholder": "Komutun nereye eklenmesini istersiniz?", + "useDefaultIcon": "Varsayılan Simgeyi Kullan" + }, + "typehierarchy": { + "subtypeHierarchy": "Alt Tip Hiyerarşisi", + "supertypeHierarchy": "Süper Tip Hiyerarşisi" + }, + "vsx-registry": { + "confirmDialogMessage": "\"{0}\" uzantısı doğrulanmamıştır ve güvenlik riski oluşturabilir.", + "confirmDialogTitle": "Kuruluma devam etmek istediğinizden emin misiniz?", + "downloadCount": "İndirme sayısı: {0}", + "errorFetching": "Uzantılar getirilirken hata oluştu.", + "errorFetchingConfigurationHint": "Bunun nedeni ağ yapılandırma sorunları olabilir.", + "failedInstallingVSIX": "VSIX'ten {0} yüklenemedi.", + "invalidVSIX": "Seçilen dosya geçerli bir \"*.vsix\" eklentisi değil.", + "license": "Ruhsat: {0}", + "onlyShowVerifiedExtensionsDescription": "Bu, {0} adresinin yalnızca doğrulanmış uzantıları göstermesini sağlar.", + "onlyShowVerifiedExtensionsTitle": "Yalnızca Doğrulanmış Uzantıları Göster", + "recommendedExtensions": "Bu depo için önerilen uzantıları yüklemek istiyor musunuz?", + "searchPlaceholder": "Uzantıları içinde ara {0}", + "showInstalled": "Yüklü Uzantıları Göster", + "showRecommendedExtensions": "Uzantı önerileri için bildirimlerin gösterilip gösterilmeyeceğini kontrol eder.", + "vsx-extensions-contribution": { + "update-version-uninstall-error": "Uzantı kaldırılırken hata oluştu: {0}.", + "update-version-version-error": "{1}'un {0} sürümü yüklenemedi." + } + }, + "webview": { + "goToReadme": "README'ye Git", + "messageWarning": " {0} uç noktasının ana bilgisayar kalıbı `{1}` olarak değiştirildi; kalıbın değiştirilmesi güvenlik açıklarına yol açabilir. Daha fazla bilgi için `{2}` adresine bakın." + }, + "workspace": { + "compareWithEachOther": "Birbirleriyle Karşılaştırın", + "confirmDeletePermanently.description": "Çöp Kutusu kullanılarak \"{0}\" silinemedi. Bunun yerine kalıcı olarak silmek mi istiyorsunuz?", + "confirmDeletePermanently.solution": "Tercihlerde Çöp Kutusu kullanımını devre dışı bırakabilirsiniz.", + "confirmDeletePermanently.title": "Dosya silinirken hata oluştu", + "confirmMessage.delete": "Aşağıdaki dosyaları gerçekten silmek istiyor musunuz?", + "confirmMessage.dirtyMultiple": "Kaydedilmemiş değişiklikler içeren {0} dosyalarını gerçekten silmek istiyor musunuz?", + "confirmMessage.dirtySingle": "Kaydedilmemiş değişikliklerle {0} adresini gerçekten silmek istiyor musunuz?", + "confirmMessage.uriMultiple": "Gerçekten tüm {0} seçili dosyaları silmek istiyor musunuz?", + "confirmMessage.uriSingle": "{0} adresini gerçekten silmek istiyor musunuz?", + "duplicate": "Yinelenen", + "failSaveAs": "Geçerli widget için \"{0}\" çalıştırılamıyor.", + "newFilePlaceholder": "Dosya Adı", + "newFolderPlaceholder": "Klasör adı", + "noErasure": "Not: Diskten hiçbir şey silinmeyecektir", + "openRecentPlaceholder": "Açmak istediğiniz çalışma alanının adını yazın", + "openRecentWorkspace": "Son Çalışma Alanını Aç...", + "preserveWindow": "Geçerli pencerede çalışma alanlarını açmayı etkinleştirir.", + "removeFolder": "Aşağıdaki klasörü çalışma alanından kaldırmak istediğinizden emin misiniz?", + "removeFolders": "Aşağıdaki klasörleri çalışma alanından kaldırmak istediğinizden emin misiniz?", + "trashTitle": "{0} adresini Çöp Kutusuna Taşı", + "trustEmptyWindow": "Varsayılan olarak boş çalışma alanına güvenilip güvenilmeyeceğini denetler.", + "trustEnabled": "Çalışma alanı güveninin etkin olup olmadığını denetler. Devre dışı bırakılırsa, tüm çalışma alanlarına güvenilir.", + "trustRequest": "Bir uzantı çalışma alanı güveni talep ediyor ancak ilgili API henüz tam olarak desteklenmiyor. Bu çalışma alanına güvenmek istiyor musunuz?", + "untitled-cleanup": "Çok sayıda başlıksız çalışma alanı dosyası var gibi görünüyor. Lütfen {0} adresini kontrol edin ve kullanılmayan dosyaları kaldırın.", + "workspaceFolderAdded": "Birden fazla kökü olan bir çalışma alanı oluşturuldu. Çalışma alanı yapılandırmanızı bir dosya olarak kaydetmek istiyor musunuz?", + "workspaceFolderAddedTitle": "Çalışma Alanına eklenen klasör" + } + } +} diff --git a/packages/core/i18n/nls.zh-cn.json b/packages/core/i18n/nls.zh-cn.json index 873735f09c054..43694260cfc3f 100644 --- a/packages/core/i18n/nls.zh-cn.json +++ b/packages/core/i18n/nls.zh-cn.json @@ -1,5 +1,11 @@ { + "aiConfiguration:open": "打开 AI 配置视图", + "aiHistory:open": "打开人工智能历史视图", "debug.breakpoint.editCondition": "编辑条件...", + "notebook.cell.changeToCode": "将单元格更改为代码", + "notebook.cell.changeToMarkdown": "将 Cell 改为 Mardown", + "notebook.cell.insertMarkdownCellAbove": "在上方插入 Markdown 单元格", + "notebook.cell.insertMarkdownCellBelow": "在下面插入 Markdown 单元格", "terminal:new:profile": "从配置文件中创建新的集成终端", "terminal:profile:default": "选择默认的终端配置文件", "theia": { @@ -7,6 +13,33 @@ "noCallers": "没有发现调用者。", "open": "打开调用层次结构" }, + "collaboration": { + "collaborate": "合作", + "collaboration": "合作", + "collaborationWorkspace": "协作工作区", + "connected": "已连接", + "connectedSession": "连接到协作会议", + "copiedInvitation": "邀请函代码已复制到剪贴板。", + "copyAgain": "再次复制", + "createRoom": "创建新的协作会话", + "creatingRoom": "创建会话", + "end": "结束合作会议", + "endDetail": "终止会话,停止内容共享,并取消其他人的访问权限。", + "enterCode": "输入协作会话代码", + "failedCreate": "创建房间失败:{0}", + "failedJoin": "未能加入房间:{0}", + "invite": "邀请他人", + "inviteDetail": "复制邀请代码,与他人分享,参加会议。", + "joinRoom": "参加协作会议", + "joiningRoom": "加入会议", + "leave": "离开合作会议", + "leaveDetail": "断开当前协作会话并关闭工作区。", + "selectCollaboration": "选择协作选项", + "sharedSession": "共享合作会议", + "startSession": "开始或加入协作会议", + "userWantsToJoin": "用户 '{0}' 希望加入协作室", + "whatToDo": "您想与其他合作者做些什么?" + }, "core": { "about": { "compatibility": "{0} 兼容性", @@ -65,6 +98,13 @@ "next": "下一页 (向下)", "previous": "上一页 (向上)" }, + "secondaryWindow": { + "alwaysOnTop": "启用后,辅助窗口将保持在所有其他窗口(包括不同应用程序的窗口)之上。", + "description": "设置提取的辅助窗口的初始位置和大小。", + "fullSize": "提取部件的位置和大小将与运行中的 Theia 应用程序相同。", + "halfWidth": "提取部件的位置和大小将是运行中的 Theia 应用程序宽度的一半。", + "originalSize": "提取部件的位置和大小将与原始部件相同。" + }, "silentNotifications": "控制是否抑制弹出通知。", "tabDefaultSize": "指定标签的默认尺寸。", "tabMaximize": "控制是否在双击时最大化标签。", @@ -94,14 +134,32 @@ "toggleTracing": "启用/禁用与调试适配器的跟踪通信" }, "editor": { + "diffEditor.wordWrap2": "行将根据 `#editor.wordWrap#` 设置换行。", "dirtyEncoding": "该文件是脏的。请先保存它,然后用另一种编码重新打开它。", - "editor.codeActionWidget.showHeaders": "启用/禁用在代码动作菜单中显示组标题。", - "editor.experimental.pasteActions.enabled": "启用/禁用粘贴时从扩展程序运行编辑。", + "editor.accessibilitySupport0": "使用平台 API 检测屏幕阅读器是否已连接", + "editor.accessibilitySupport1": "优化屏幕阅读器的使用", + "editor.accessibilitySupport2": "假设未连接屏幕阅读器", + "editor.bracketPairColorization.enabled": "控制是否启用括号对着色。使用 `#workbench.colorCustomizations#` 覆盖括号高亮颜色。", + "editor.codeActionWidget.includeNearbyQuickfixes": "启用/禁用在当前未进行诊断时显示行内最近的快速修复。", + "editor.cursorSurroundingLinesStyle": "控制何时执行 `#cursorSurroundingLines#` 。", + "editor.detectIndentation": "控制打开文件时是否根据文件内容自动检测 `#editor.tabSize#` 和 `#editor.insertSpaces#`。", + "editor.dropIntoEditor.enabled": "控制是否可以按住 `shift` 将文件拖放到文本编辑器中(而不是在编辑器中打开文件)。", "editor.formatOnSaveMode.modificationsIfAvailable": "将尝试只对修改部分进行格式化(需要源代码控制)。如果不能使用源码控制,那么整个文件将被格式化。", + "editor.hover.hidingDelay": "控制隐藏悬停后的延迟时间(毫秒)。需要启用 `editor.hover.sticky`。", "editor.inlayHints.enabled1": "镶嵌提示默认显示,按住 \"Ctrl+Alt \"时隐藏。", "editor.inlayHints.enabled2": "镶嵌提示默认是隐藏的,当按住`Ctrl+Alt`时显示。", + "editor.inlayHints.fontFamily": "控制编辑器中镶嵌提示的字体家族。设置为空时,将使用 `#editor.fontFamily#`。", + "editor.inlayHints.fontSize": "控制编辑器中镶嵌提示的字体大小。默认情况下,当配置值小于 \"5 \"或大于编辑器字体大小时,将使用 \"#editor.fontSize#\"。", + "editor.insertSpaces": "按 `Tab` 键时插入空格。当启用 `#editor.detectIndentation#`时,该设置会根据文件内容被覆盖。", + "editor.occurrencesHighlight": "控制编辑器是否高亮显示语义符号。", "editor.quickSuggestions": "控制在输入时是否应该自动显示建议。这可以在输入评论、字符串和其他代码时加以控制。快速建议可以被配置为显示为幽灵文本或建议小部件。还要注意'#editor.suggestOnTriggerCharacters#'设置,它控制建议是否被特殊字符触发。", - "editor.suggest.matchOnWordStartOnly": "当启用IntelliSense过滤功能时,需要在一个词的开头匹配第一个字符,例如在 \"Console \"或 \"WebContext \"上的 \"c\",但在 \"description \"上_不需要。当禁用IntelliSense时,将显示更多的结果,但仍然按照匹配质量进行排序。", + "editor.stickyScroll.scrollWithEditor": "使用编辑器的水平滚动条启用粘性滚动 widget 的滚动功能。", + "editor.suggestFontSize": "建议 widget 的字体大小。设置为 \"0 \"时,将使用 \"#editor.fontSize#\"的值。", + "editor.suggestLineHeight": "建议 widget 的行高。设置为 \"0 \"时,将使用 \"#editor.lineHeight#\"的值。最小值为 8。", + "editor.tabSize": "制表符等于的空格数。当启用 `#editor.detectIndentation#`时,该设置会根据文件内容被覆盖。", + "editor.useTabStops": "在制表符后插入和删除空白。", + "editor.wordBasedSuggestions": "控制是否根据文档中的单词计算补全。", + "editor.wordBasedSuggestionsMode": "控制从哪些文档中计算基于单词的补全。", "files.autoSave": "控制有未保存的修改的编辑器的[自动保存](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save)。", "files.autoSave.afterDelay": "在配置的 `#files.autoSaveDelay#`之后,有改动的编辑器会自动保存。", "files.autoSave.off": "有变化的编辑器永远不会被自动保存。", @@ -164,7 +222,7 @@ "git": { "aFewSecondsAgo": "几秒钟前", "addSignedOff": "添加 \"已签署\"。", - "amendReuseMessag": "要重新使用最后一条提交信息,请按'Enter'或'Escape'来取消。", + "amendReuseMessage": "要重新使用最后一条提交信息,请按'Enter'或'Escape'来取消。", "amendRewrite": "重写之前的提交信息。按'Enter'键确认或按'Escape'键取消。", "checkoutCreateLocalBranchWithName": "创建一个新的本地分支,名称为:{0}。按'Enter'键确认或按'Escape'键取消。", "checkoutProvideBranchName": "请提供分支机构名称。", @@ -333,6 +391,9 @@ "alreadyRunning": "托管的实例已经在运行。", "debugInstance": "调试实例", "debugMode": "使用 inspect 或 inspect-brk 进行 Node.js 调试", + "debugPorts": { + "debugPort": "用于此服务器 Node.js 调试的端口" + }, "devHost": "发展的主人", "failed": "运行托管插件实例失败。{0}", "hostedPlugin": "托管的插件", @@ -364,6 +425,10 @@ "webviewTrace": "控制与webviews的通信跟踪。", "webviewWarnIfUnsecure": "警告用户,目前网络视图的部署是不安全的。" }, + "preferences": { + "hostedPlugin": "托管插件", + "toolbar": "工具栏" + }, "preview": { "openByDefault": "默认情况下,打开预览而不是编辑器。" }, @@ -386,6 +451,9 @@ "config.untrackedChanges.hidden": "隐藏的", "config.untrackedChanges.mixed": "混合的", "config.untrackedChanges.separate": "分开", + "dirtyDiff": { + "close": "关闭 更改 窥视" + }, "history": "历史", "noRepositoryFound": "没有找到存储库", "unamend": "撤销", @@ -401,8 +469,7 @@ "extract-widget": "将视图移至第二窗口" }, "shell-area": { - "secondary": "二级窗口", - "top": "返回顶部" + "secondary": "二级窗口" }, "task": { "attachTask": "附加任务...", @@ -422,6 +489,7 @@ "profilePath": "此配置文件使用的shell的路径。", "profiles": "创建一个新的终端时要呈现的配置文件。用可选的args手动设置路径属性。\n将现有的配置文件设置为`null'以从列表中隐藏该配置文件,例如:`\"{0}\": null`。", "rendererType": "控制终端的渲染方式。", + "rendererTypeDeprecationMessage": "不再支持将呈现器类型作为选项。", "selectProfile": "为新终端选择一个配置文件", "shell.deprecated": "这已被废弃,新的推荐方法是在'terminal.integrated.profiles.{0}'中创建一个终端配置文件,并在'terminal.integrated.defaultProfile.{0}'中把它的配置文件名称设置为默认值,来配置你的默认外壳。", "shellArgsLinux": "在Linux终端时使用的命令行参数。", @@ -433,6 +501,7 @@ }, "test": { "cancelAllTestRuns": "取消所有测试运行", + "stackFrameAt": "于", "testRunDefaultName": "{0} 运行{1}", "testRuns": "测试运行" }, @@ -461,12 +530,16 @@ "supertypeHierarchy": "超类型层次结构" }, "vsx-registry": { + "confirmDialogMessage": "扩展名 \"{0}\" 未经验证,可能存在安全风险。", + "confirmDialogTitle": "您确定要继续安装吗?", "downloadCount": "下载次数: {0}", "errorFetching": "取出扩展程序时出错。", "errorFetchingConfigurationHint": "这可能是网络配置问题造成的。", "failedInstallingVSIX": "从VSIX安装{0} ,失败了。", "invalidVSIX": "所选文件不是有效的 \"*.vsix \"插件。", "license": "许可证: {0}", + "onlyShowVerifiedExtensionsDescription": "这样,{0} 只显示经过验证的扩展名。", + "onlyShowVerifiedExtensionsTitle": "只显示已验证的扩展名", "recommendedExtensions": "建议在该工作区使用的扩展名称的列表。", "searchPlaceholder": "在{0}中搜索扩展", "showInstalled": "显示已安装的扩展程序", @@ -491,7 +564,6 @@ "confirmMessage.uriMultiple": "你真的想删除所有{0}选中的文件吗?", "confirmMessage.uriSingle": "你真的想删除{0}吗?", "duplicate": "复制", - "failApply": "无法将更改应用于新文件", "failSaveAs": "无法为当前的小组件运行\"{0}\"。", "newFilePlaceholder": "文件名称", "newFolderPlaceholder": "文件夹名称", diff --git a/packages/core/i18n/nls.zh-tw.json b/packages/core/i18n/nls.zh-tw.json new file mode 100644 index 0000000000000..ede1123f822d0 --- /dev/null +++ b/packages/core/i18n/nls.zh-tw.json @@ -0,0 +1,585 @@ +{ + "aiConfiguration:open": "開啟 AI 設定檢視", + "aiHistory:open": "開啟 AI 歷史檢視", + "debug.breakpoint.editCondition": "編輯條件...", + "notebook.cell.changeToCode": "變更儲存格為代碼", + "notebook.cell.changeToMarkdown": "變更儲存格為 Markdown", + "notebook.cell.insertMarkdownCellAbove": "在上方插入 Markdown 單元格", + "notebook.cell.insertMarkdownCellBelow": "在下方插入 Markdown 單元格", + "terminal:new:profile": "從設定檔建立新的整合式終端機", + "terminal:profile:default": "選擇預設終端設定檔", + "theia": { + "callhierarchy": { + "noCallers": "未偵測到來電者。", + "open": "開放式呼叫層級" + }, + "collaboration": { + "collaborate": "合作", + "collaboration": "合作", + "collaborationWorkspace": "協同工作區", + "connected": "連接", + "connectedSession": "連接至協作會議", + "copiedInvitation": "邀請函代碼已複製到剪貼簿。", + "copyAgain": "再次複製", + "createRoom": "建立新的協作會議", + "creatingRoom": "創建會話", + "end": "結束協作會議", + "endDetail": "終止會話、停止內容共用,並取消其他人的存取權限。", + "enterCode": "輸入協作會議代碼", + "failedCreate": "建立空間失敗:{0}", + "failedJoin": "未能加入房間:{0}", + "invite": "邀請他人", + "inviteDetail": "複製邀請代碼,以便與他人分享,參加會議。", + "joinRoom": "加入協作會議", + "joiningRoom": "加入會議", + "leave": "休假協作會議", + "leaveDetail": "中斷目前的協作工作階段,並關閉工作區。", + "selectCollaboration": "選擇合作選項", + "sharedSession": "分享協作會議", + "startSession": "開始或加入協作會議", + "userWantsToJoin": "使用者 '{0}' 想要加入協作室", + "whatToDo": "您想與其他合作者做什麼?" + }, + "core": { + "about": { + "compatibility": "{0} 相容性", + "defaultApi": "預設{0} API", + "version": "版本" + }, + "common": { + "closeAll": "關閉所有標籤", + "closeAllTabMain": "關閉主區域中的所有標籤", + "closeOtherTabMain": "關閉主區中的其他標籤", + "closeOthers": "關閉其他標籤", + "closeRight": "向右關閉標籤", + "closeTab": "關閉標籤", + "closeTabMain": "關閉主區域中的標籤", + "collapseAllTabs": "折疊所有側板", + "collapseBottomPanel": "切換底部面板", + "collapseTab": "折疊側板", + "showNextTabGroup": "切換到下一個標籤組", + "showNextTabInGroup": "切換到群組中的下一個標籤", + "showPreviousTabGroup": "切換到上一個標籤組", + "showPreviousTabInGroup": "切換到群組中的上一個標籤", + "toggleMaximized": "切換最大化" + }, + "copyInfo": "先開啟檔案以複製其路徑", + "copyWarn": "請使用瀏覽器的複製指令或捷徑。", + "cutWarn": "請使用瀏覽器的剪下指令或捷徑。", + "enhancedPreview": { + "classic": "顯示標籤的簡單預覽及基本資訊。", + "enhanced": "顯示標籤的增強預覽,並提供其他資訊。", + "visual": "顯示標籤的視覺預覽。" + }, + "file": { + "browse": "瀏覽" + }, + "highlightModifiedTabs": "控制是否在已修改(髒)的編輯器標籤上畫上邊框。", + "keybindingStatus": "{0} 被按下,等待更多的按鍵", + "keyboard": { + "choose": "選擇鍵盤配置", + "chooseLayout": "選擇鍵盤配置", + "current": "(目前:{0})", + "currentLayout": "- 目前佈局", + "mac": "Mac 鍵盤", + "pc": "PC 鍵盤", + "tryDetect": "嘗試從瀏覽器資訊和按下的按鍵偵測鍵盤配置。" + }, + "navigator": { + "clipboardWarn": "拒絕存取剪貼板。檢查瀏覽器的權限。", + "clipboardWarnFirefox": "剪貼板 API 不可用。您可以在 '{1}' 頁面上的 '{0}' 偏好設定啟用。然後重新載入 Theia。請注意,這將允許 FireFox 完全存取系統剪貼板。" + }, + "offline": "離線", + "pasteWarn": "請使用瀏覽器的貼上指令或捷徑。", + "quitMessage": "任何未儲存的變更都不會儲存。", + "resetWorkbenchLayout": "重設工作台佈局", + "searchbox": { + "close": "關閉(逃生)", + "next": "下一頁 (向下)", + "previous": "上一頁 (上)" + }, + "secondaryWindow": { + "alwaysOnTop": "啟用時,次要視窗會停留在所有其他視窗之上,包括不同應用程式的視窗。", + "description": "設定抽取的次要視窗的初始位置和大小。", + "fullSize": "擷取的 widget 位置和大小將與執行中的 Theia 應用程式相同。", + "halfWidth": "擷取的 widget 位置和大小將是執行中的 Theia 應用程式寬度的一半。", + "originalSize": "提取出來的 widget 的位置和大小將與原始 widget 相同。" + }, + "silentNotifications": "控制是否抑制通知彈出。", + "tabDefaultSize": "指定標籤的預設大小。", + "tabMaximize": "控制是否在雙擊時將索引標籤最大化。", + "tabMinimumSize": "指定標籤頁的最小尺寸。", + "tabShrinkToFit": "縮小標籤以符合可用空間。" + }, + "debug": { + "addConfigurationPlaceholder": "選擇要新增組態的工作區根", + "breakpoint": "断点", + "compound-cycle": "啟動組態 '{0}' 包含與本身的循環", + "continueAll": "繼續全部", + "copyExpressionValue": "複製表達值", + "dataBreakpoint": "資料中斷點", + "debugVariableInput": "設定{0} 值", + "entry": "入口", + "exception": "例外", + "functionBreakpoint": "函數中斷點", + "goto": "到達", + "instruction-breakpoint": "指令中斷點", + "instructionBreakpoint": "指令中斷點", + "missingConfiguration": "動態設定 '{0}:{1}' 遺失或不適用", + "pause": "暫停", + "pauseAll": "暫停全部", + "reveal": "揭示", + "step": "步驟", + "threads": "線程", + "toggleTracing": "啟用/停用與除錯介面卡的追蹤通訊" + }, + "editor": { + "diffEditor.wordWrap2": "行會根據 `#editor.wordWrap#` 設定來換行。", + "dirtyEncoding": "檔案已損壞。請先儲存檔案,再以其他編碼重新開啟。", + "editor.accessibilitySupport0": "使用平台 API 來偵測是否已連接螢幕閱讀器", + "editor.accessibilitySupport1": "針對螢幕閱讀器的使用進行最佳化", + "editor.accessibilitySupport2": "假設沒有連接螢幕閱讀器", + "editor.bracketPairColorization.enabled": "控制是否啟用括號對著色。使用 `#workbench.colorCustomizations#` 來覆寫括弧高亮顏色。", + "editor.codeActionWidget.includeNearbyQuickfixes": "當目前未進行診斷時,啟用/停用在行內顯示最近的快速修復。", + "editor.cursorSurroundingLinesStyle": "控制何時執行 `#cursorSurroundingLines#`。", + "editor.detectIndentation": "控制在開啟檔案時,是否會根據檔案內容自動偵測 `#editor.tabSize#` 和 `#editor.insertSpaces#`。", + "editor.dropIntoEditor.enabled": "控制是否可以按住 `shift` 將檔案拖放到文字編輯器中 (而不是在編輯器中開啟檔案)。", + "editor.formatOnSaveMode.modificationsIfAvailable": "僅嘗試格式化修改 (需要來源控制)。如果無法使用原始碼控制,則會格式化整個檔案。", + "editor.hover.hidingDelay": "控制以毫秒為單位的延遲時間,之後懸浮會隱藏。需要啟用 `editor.hover.sticky`。", + "editor.inlayHints.enabled1": "鑲嵌提示依預設顯示,並在按住 Ctrl+Alt 時隱藏", + "editor.inlayHints.enabled2": "內嵌提示預設為隱藏,按住 Ctrl+Alt 時會顯示。", + "editor.inlayHints.fontFamily": "控制編輯器中內嵌提示的字型族。設定為空時,會使用 `#editor.fontFamily#`。", + "editor.inlayHints.fontSize": "控制編輯器中內嵌提示的字型大小。當設定值小於或大於編輯器字型大小時,預設會使用 `#editor.fontSize#`。", + "editor.insertSpaces": "按下 `Tab` 時插入空格。當 `#editor.detectIndentation#` 啟用時,此設定會根據檔案內容覆寫。", + "editor.occurrencesHighlight": "控制編輯器是否要高亮顯示語意符號的出現。", + "editor.quickSuggestions": "控制輸入時是否自動顯示建議。在輸入註解、字串和其他程式碼時,可以控制這一點。快速建議可設定為以 ghost text 或建議 widget 顯示。同時也要注意 `#editor.supplyOnTriggerCharacters#` 設定,它可以控制是否由特殊字符觸發建議。", + "editor.stickyScroll.scrollWithEditor": "使用編輯器的水平捲動條啟用黏貼捲動 widget 的捲動。", + "editor.suggestFontSize": "建議 Widget 的字型大小。設定為 `0` 時,會使用 `#editor.fontSize#` 的值。", + "editor.suggestLineHeight": "建議 widget 的行高。設定為 `0` 時,會使用 `#editor.lineHeight#` 的值。最小值為 8。", + "editor.tabSize": "制表符等於的空格數目。當 `#editor.detectIndentation#` 啟用時,此設定會根據檔案內容覆寫。", + "editor.useTabStops": "插入和刪除空白跟著制表符停止。", + "editor.wordBasedSuggestions": "控制是否應根據文件中的字詞計算完成度。", + "editor.wordBasedSuggestionsMode": "控制從哪些文件中計算出基於字詞的完成度。", + "files.autoSave": "控制有未儲存變更的編輯器 [自動儲存](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save)。", + "files.autoSave.afterDelay": "有變更的編輯器會在設定的 `#files.autoSaveDelay#` 後自動儲存。", + "files.autoSave.off": "有變更的編輯器不會自動儲存。", + "files.autoSave.onFocusChange": "當編輯器失去焦點時,會自動儲存有變更的編輯器。", + "files.autoSave.onWindowChange": "當視窗失去焦點時,會自動儲存有變更的編輯器。", + "formatOnSaveTimeout": "以毫秒為單位的逾時,逾時後檔案儲存時執行的格式化會被取消。", + "persistClosedEditors": "控制是否在視窗重新載入時持續工作區的已關閉編輯歷史。", + "showAllEditors": "顯示所有開啟的編輯器", + "splitHorizontal": "水平分割編輯器", + "splitVertical": "垂直分割編輯器", + "toggleStickyScroll": "切換黏貼捲動" + }, + "file-search": { + "toggleIgnoredFiles": "(按{0} 顯示/隱藏忽略的檔案)" + }, + "fileDialog": { + "showHidden": "顯示隱藏的檔案" + }, + "fileSystem": { + "fileResource": { + "overWriteBody": "您要覆寫檔案系統上對 '{0}' 所做的變更?" + } + }, + "filesystem": { + "copiedToClipboard": "將下載連結複製到剪貼簿。", + "copyDownloadLink": "複製下載連結", + "dialog": { + "initialLocation": "前往初始位置", + "multipleItemMessage": "您只能選擇一個項目", + "name": "名稱:", + "navigateBack": "導航返回", + "navigateForward": "向前導航", + "navigateUp": "向上瀏覽一個目錄" + }, + "fileResource": { + "binaryFileQuery": "打開它可能需要一些時間,而且可能會導致 IDE 無反應。您到底要不要開啟 '{0}' 呢?", + "binaryTitle": "檔案為二進位或使用不支援的文字編碼。", + "largeFileTitle": "檔案太大 ({0})。", + "overwriteTitle": "檔案系統中的檔案 '{0}' 已變更。" + }, + "filesExclude": "設定 glob 模式以排除檔案和資料夾。例如,檔案總管會根據此設定決定顯示或隱藏哪些檔案和資料夾。", + "format": "格式:", + "maxConcurrentUploads": "上傳多個檔案時,同時上傳的最大檔案數量。0 表示所有檔案都會同時上傳。", + "maxFileSizeMB": "控制可開啟的最大檔案大小 (以 MB 為單位)。", + "prepareDownload": "準備下載...", + "prepareDownloadLink": "準備下載連結...", + "processedOutOf": "加工{0} 出{1}", + "replaceTitle": "取代檔案", + "uploadFiles": "上傳檔案...", + "uploadedOutOf": "上傳{0} 出{1}" + }, + "getting-started": { + "apiComparator": "{0} API 相容性", + "newExtension": "建立新擴展區", + "newPlugin": "建立新的外掛程式", + "startup-editor": { + "welcomePage": "開啟歡迎頁面,其內容可協助您開始使用{0} 和擴充套件。" + } + }, + "git": { + "aFewSecondsAgo": "數秒前", + "addSignedOff": "新增簽署人", + "amendReuseMessage": "若要重複使用上次的提交訊息,請按「Enter」或「Escape」取消。", + "amendRewrite": "重寫先前的提交訊息。按「Enter」確認或按「Escape」取消。", + "checkoutCreateLocalBranchWithName": "建立新的本地分支,名稱為{0}。按「Enter」確認或按「Escape」取消。", + "checkoutProvideBranchName": "請提供分行名稱。 ", + "checkoutSelectRef": "選擇要結帳的參考資料或建立新的本地分支:", + "cloneQuickInputLabel": "請提供 Git 儲存庫位置。按「Enter」確認或按「Escape」取消。", + "cloneRepository": "克隆 Git 倉庫:{0}。按「Enter」確認或按「Escape」取消。", + "compareWith": "比較...", + "compareWithBranchOrTag": "選取分支或標籤,與目前使用中的{0} 分支進行比較:", + "diff": "差異", + "dirtyDiffLinesLimit": "如果編輯器的行數超過此限制,則不顯示髒的差異裝飾。", + "dropStashMessage": "成功移除藏匿物。", + "editorDecorationsEnabled": "在編輯器中顯示 git 裝飾。", + "fetchPickRemote": "選取遠端擷取:", + "gitDecorationsColors": "在導覽器中使用顏色裝飾。", + "mergeQuickPickPlaceholder": "選取一個分支合併到目前使用中的{0} 分支:", + "missingUserInfo": "請確定您在 git 中設定了「user.name」和「user.email」。", + "noHistoryForError": "沒有關於{0}", + "noPreviousCommit": "之前沒有承諾修正", + "noRepositoriesSelected": "未選擇儲存庫。", + "prepositionIn": "於", + "repositoryNotInitialized": "儲存庫{0} 尚未初始化。", + "stashChanges": "儲存變更。按「Enter」確認,或按「Escape」取消。", + "stashChangesWithMessage": "儲存庫隨訊息變更:{0}.按「Enter」確認或按「Escape」取消。", + "tabTitleIndex": "{0} (索引)", + "tabTitleWorkingTree": "{0} (工作樹)", + "toggleBlameAnnotations": "切換指責註釋" + }, + "keybinding-schema-updater": { + "deprecation": "改用 `when` 子句。" + }, + "keymaps": { + "addKeybindingTitle": "新增鍵盤綁定{0}", + "editKeybinding": "編輯按鍵...", + "editKeybindingTitle": "編輯{0}", + "editWhenExpression": "編輯當表達...", + "editWhenExpressionTitle": "編輯{0}", + "keybinding": { + "copy": "複製鍵綁定", + "copyCommandId": "複製鍵綁定指令 ID", + "copyCommandTitle": "複製鍵綁定指令標題", + "edit": "編輯按鍵...", + "editWhenExpression": "編輯按鍵綁定當表達..." + }, + "keybindingCollidesValidation": "按鍵目前碰撞", + "requiredKeybindingValidation": "需要 keybinding 值", + "resetKeybindingConfirmation": "您真的要將此按鍵綁定重設為預設值?", + "resetKeybindingTitle": "重設{0}", + "resetMultipleKeybindingsWarning": "如果此命令存在多個按鍵綁定,則所有按鍵綁定都會被重設。" + }, + "localize": { + "offlineTooltip": "無法連線至後端。" + }, + "markers": { + "clearAll": "全部清除", + "noProblems": "到目前為止,工作區尚未偵測到任何問題。", + "tabbarDecorationsEnabled": "在標籤列中顯示問題裝飾符(診斷標記)。" + }, + "memory-inspector": { + "addressTooltip": "要顯示的記憶體位置、位址或演算至位址的表達式", + "ascii": "ASCII", + "binary": "二進制", + "byteSize": "位元組大小", + "bytesPerGroup": "每組位元組", + "closeSettings": "關閉設定", + "columns": "欄位", + "command": { + "createNewMemory": "建立新的記憶體檢查器", + "createNewRegisterView": "建立新的註冊檢視", + "followPointer": "追蹤指針", + "followPointerMemory": "在記憶體檢視器中追蹤指針", + "resetValue": "重設值", + "showRegister": "在記憶體檢查器中顯示暫存器", + "viewVariable": "在記憶體檢視器中顯示變數" + }, + "data": "資料", + "decimal": "十進制", + "diff": { + "label": "差異:{0}" + }, + "diff-widget": { + "offset-label": "{0} 偏移", + "offset-title": "偏移記憶體的位元組{0}" + }, + "editable": { + "apply": "應用變更", + "clear": "清楚的變更" + }, + "endianness": "尾數", + "extraColumn": "額外欄位", + "groupsPerRow": "每行的群組", + "hexadecimal": "十六進位", + "length": "長度", + "lengthTooltip": "要取得的位元組數量,以十進位或十六進位表示", + "memory": { + "addressField": { + "memoryReadError": "在 Location(位置)欄位中輸入地址或表達式。" + }, + "freeze": "凍結記憶體檢視", + "hideSettings": "隱藏設定面板", + "readError": { + "bounds": "超出記憶體範圍,結果會被截斷。", + "noContents": "目前沒有可用的記憶體內容。" + }, + "readLength": { + "memoryReadError": "在 Length(長度)欄位中輸入長度(十進制或十六進制數字)。" + }, + "showSettings": "顯示設定面板", + "unfreeze": "解除凍結記憶體檢視", + "userError": "取得記憶體時發生錯誤。" + }, + "memoryCategory": "記憶體檢查器", + "memoryInspector": "記憶體檢查器", + "memoryTitle": "記憶體", + "octal": "八進制", + "offset": "偏移", + "offsetTooltip": "當導航時,要加到目前記憶體位置的偏移量", + "provider": { + "localsError": "無法讀取本機變數。沒有啟動除錯階段。", + "readError": "無法讀取記憶體。沒有作用中的除錯會話。", + "writeError": "無法寫入記憶體。沒有作用中的除錯會話。" + }, + "register": "註冊", + "register-widget": { + "filter-placeholder": "過濾器 (以)" + }, + "registerReadError": "取得暫存器時發生錯誤。", + "registers": "註冊", + "toggleComparisonWidgetVisibility": "切換比較小工具的可見性", + "utils": { + "afterBytes": "您必須在要比較的兩個 widget 中都載入記憶體。{0} 沒有載入記憶體。", + "bytesMessage": "您必須在要比較的兩個 widget 中都載入記憶體。{0} 沒有載入記憶體。" + } + }, + "messages": { + "notificationTimeout": "超時後,資訊性通知將會隱藏。", + "toggleNotifications": "切換通知" + }, + "mini-browser": { + "typeUrl": "輸入 URL" + }, + "monaco": { + "noSymbolsMatching": "沒有符合的符號", + "typeToSearchForSymbols": "輸入以搜尋符號" + }, + "navigator": { + "autoReveal": "自動揭示", + "clipboardWarn": "拒絕存取剪貼板。檢查瀏覽器的權限。", + "clipboardWarnFirefox": "剪貼板 API 不可用。您可以在 '{1}' 頁面上的 '{0}' 偏好設定啟用。然後重新載入 Theia。請注意,這將允許 FireFox 完全存取系統剪貼板。", + "refresh": "在瀏覽器中重新整理", + "reveal": "在瀏覽器中顯示", + "toggleHiddenFiles": "切換隱藏檔案" + }, + "output": { + "clearOutputChannel": "清除輸出通道...", + "closeOutputChannel": "關閉輸出通道...", + "hiddenChannels": "隱藏通道", + "hideOutputChannel": "隱藏輸出通道...", + "maxChannelHistory": "輸出通道中的最大項目數量。", + "outputChannels": "輸出通道", + "showOutputChannel": "顯示輸出通道..." + }, + "plugin": { + "blockNewTab": "您的瀏覽器無法開啟新標籤頁" + }, + "plugin-dev": { + "alreadyRunning": "托管实例已在运行。", + "debugInstance": "除錯實例", + "debugMode": "使用 inspect 或 inspect-brk 進行 Node.js 除錯", + "debugPorts": { + "debugPort": "此伺服器的 Node.js 除錯要使用的連接埠" + }, + "devHost": "開發主機", + "failed": "執行虛擬外掛實例失敗:{0}", + "hostedPlugin": "託管外掛程式", + "hostedPluginRunning": "託管外掛程式: 執行中", + "hostedPluginStarting": "託管外掛程式:啟動", + "hostedPluginStopped": "託管外掛程式:已停止", + "hostedPluginWatching": "託管外掛程式:觀看", + "instanceTerminated": "{0} 已終止", + "launchOutFiles": "用於定位產生的 JavaScript 檔案的 glob 模式陣列 (`${pluginPath}`將被 plugin 的實際路徑取代)。", + "noValidPlugin": "指定的資料夾不包含有效的外掛程式。", + "notRunning": "托管实例未运行。", + "pluginFolder": "外掛程式資料夾設定為:{0}", + "preventedNewTab": "您的瀏覽器無法開啟新標籤頁", + "restartInstance": "重新啟動實體", + "running": "主機實例執行於:", + "select": "選擇", + "selectPath": "選擇路徑", + "startInstance": "啟動實例", + "starting": "啟動託管實例伺服器 ...", + "stopInstance": "停止實例", + "unknownTerminated": "實例已終止", + "watchMode": "在開發中的外掛程式上執行觀察程式" + }, + "plugin-ext": { + "authentication-main": { + "loginTitle": "登入" + }, + "plugins": "外掛程式", + "webviewTrace": "控制 webviews 的通訊追蹤。", + "webviewWarnIfUnsecure": "警告使用者目前以不安全的方式部署 webview。" + }, + "preferences": { + "hostedPlugin": "託管外掛程式", + "toolbar": "工具列" + }, + "preview": { + "openByDefault": "預設開啟預覽而非編輯器。" + }, + "property-view": { + "created": "創建", + "directory": "目錄", + "lastModified": "最後修改", + "location": "地點", + "noProperties": "無房產可供選擇。", + "properties": "屬性", + "size": "尺寸", + "symbolicLink": "符號連結" + }, + "scm": { + "amend": "修正", + "amendHeadCommit": "HEAD 承諾", + "amendLastCommit": "修正上次提交", + "changeRepository": "變更儲存庫...", + "config.untrackedChanges": "控制未追蹤變更的行為。", + "config.untrackedChanges.hidden": "隱藏的", + "config.untrackedChanges.mixed": "混合", + "config.untrackedChanges.separate": "獨立", + "dirtyDiff": { + "close": "關閉變更窺視" + }, + "history": "歷史", + "noRepositoryFound": "未找到儲存庫", + "unamend": "Unamend", + "unamendCommit": "取消修正提交" + }, + "search-in-workspace": { + "includeIgnoredFiles": "包含忽略的檔案", + "noFolderSpecified": "您尚未開啟或指定資料夾。目前只搜尋開啟的檔案。", + "resultSubset": "這只是所有結果的子集。請使用更明確的搜尋字詞縮小結果清單的範圍。", + "searchOnEditorModification": "修改時搜尋使用中的編輯器。" + }, + "secondary-window": { + "extract-widget": "將檢視移至次要視窗" + }, + "shell-area": { + "secondary": "輔助窗口" + }, + "task": { + "attachTask": "附加任務...", + "clearHistory": "清除歷史", + "noTaskToRun": "未找到要執行的任務。配置任務...", + "openUserTasks": "開啟使用者任務" + }, + "terminal": { + "defaultProfile": "上使用的預設設定檔{0}", + "enableCopy": "啟用 ctrl-c(macOS 上為 cmd-c)複製選取的文字", + "enablePaste": "啟用 ctrl-v(macOS 上為 cmd-v)從剪貼簿貼上", + "profileArgs": "此設定檔使用的 shell 參數。", + "profileColor": "與終端關聯的終端主題顏色 ID。", + "profileDefault": "選擇預設設定檔...", + "profileIcon": "要與終端圖示關聯的 codicon ID。\nterminal-tmux:\"$(terminal-tmux)\"", + "profileNew": "新終端機 (含簡介)...", + "profilePath": "此設定檔使用的 shell 路徑。", + "profiles": "建立新終端時要顯示的設定檔。使用可選的 args 手動設定路徑屬性。\n將現有的設定檔設定為「null」,以從清單中隱藏設定檔,例如: `\"{0}\": null`。", + "rendererType": "控制終端機的呈現方式。", + "rendererTypeDeprecationMessage": "渲染器類型不再支援為選項。", + "selectProfile": "為新終端選擇設定檔", + "shell.deprecated": "這已經被廢棄,新的建議配置預設 shell 的方式是在 'terminal.integrated.profiles.{0}' 中建立終端設定檔,並在 'terminal.integrated.defaultProfile.{0}.' 中將其設定檔名稱設定為預設值。", + "shellArgsLinux": "在 Linux 終端時要使用的命令列參數。", + "shellArgsOsx": "在 macOS 終端時要使用的命令列參數。", + "shellArgsWindows": "在 Windows 終端時要使用的命令列參數。", + "shellLinux": "終端在 Linux 上使用的 shell 路徑 (預設值: '{0}'})。", + "shellOsx": "終端在 macOS 上使用的 shell 路徑 (預設值: '{0}'})。", + "shellWindows": "終端在 Windows 上使用的 shell 路徑。(預設值:'{0}' )。" + }, + "test": { + "cancelAllTestRuns": "取消所有測試執行", + "stackFrameAt": "於", + "testRunDefaultName": "{0} 跑{1}", + "testRuns": "測試運行" + }, + "toolbar": { + "addCommand": "新增指令至工具列", + "addCommandPlaceholder": "尋找要加入工具列的指令", + "centerColumn": "中柱", + "failedUpdate": "更新 '{1}' 中 '{0}' 的值失敗。", + "filterIcons": "篩選器圖示", + "iconSelectDialog": "為 '{0}' 選擇圖示", + "iconSet": "圖示集", + "insertGroupLeft": "插入群組分隔線(左)", + "insertGroupRight": "插入群組分隔線(右)", + "leftColumn": "左欄", + "openJSON": "自訂工具列 (開啟 JSON)", + "removeCommand": "從工具列移除指令", + "restoreDefaults": "還原工具列預設值", + "rightColumn": "右欄", + "selectIcon": "選擇圖示", + "toggleToolbar": "切換工具列", + "toolbarLocationPlaceholder": "您希望在哪裡加入指令?", + "useDefaultIcon": "使用預設圖示" + }, + "typehierarchy": { + "subtypeHierarchy": "子類別階層", + "supertypeHierarchy": "超類型層級" + }, + "vsx-registry": { + "confirmDialogMessage": "副檔名 \"{0}\" 未經驗證,可能會構成安全風險。", + "confirmDialogTitle": "您確定要繼續安裝嗎?", + "downloadCount": "下載計數:{0}", + "errorFetching": "取得擴充套件時出錯。", + "errorFetchingConfigurationHint": "這可能是網路組態問題造成的。", + "failedInstallingVSIX": "從 VSIX 安裝{0} 失敗。", + "invalidVSIX": "選取的檔案不是有效的「*.vsix」外掛程式。", + "license": "許可證:{0}", + "onlyShowVerifiedExtensionsDescription": "這可讓{0} 只顯示已驗證的擴充套件。", + "onlyShowVerifiedExtensionsTitle": "只顯示已驗證的擴充套件", + "recommendedExtensions": "您要為此套件庫安裝建議的擴充套件嗎?", + "searchPlaceholder": "搜尋擴展名稱{0}", + "showInstalled": "顯示已安裝的擴充套件", + "showRecommendedExtensions": "控制是否顯示擴展建議的通知。", + "vsx-extensions-contribution": { + "update-version-uninstall-error": "移除副檔名時發生錯誤:{0}。", + "update-version-version-error": "{1} 的{0} 版本安裝失敗。" + } + }, + "webview": { + "goToReadme": "前往 README", + "messageWarning": " {0} 端點的主機模式已變更為 `{1}`;變更模式可能會導致安全漏洞。 詳情請參閱 `{2}`。" + }, + "workspace": { + "compareWithEachOther": "相互比較", + "confirmDeletePermanently.description": "使用垃圾桶刪除 \"{0}\" 失敗。您想要永久刪除嗎?", + "confirmDeletePermanently.solution": "您可以在喜好設定中停用垃圾桶的使用。", + "confirmDeletePermanently.title": "刪除檔案出錯", + "confirmMessage.delete": "您真的要刪除下列檔案嗎?", + "confirmMessage.dirtyMultiple": "您真的想要刪除{0} 檔案中未儲存的變更嗎?", + "confirmMessage.dirtySingle": "您真的要刪除{0} 未儲存的變更嗎?", + "confirmMessage.uriMultiple": "您真的要刪除所有{0} 選取的檔案?", + "confirmMessage.uriSingle": "您真的要刪除{0}?", + "duplicate": "重複", + "failSaveAs": "無法為目前的 widget 執行 \"{0}\"。", + "newFilePlaceholder": "檔案名稱", + "newFolderPlaceholder": "資料夾名稱", + "noErasure": "注意:磁碟上的任何內容都不會被刪除", + "openRecentPlaceholder": "輸入您要開啟的工作區名稱", + "openRecentWorkspace": "開啟最近工作區...", + "preserveWindow": "啟用在目前視窗中開啟工作區。", + "removeFolder": "您確定要從工作區移除下列資料夾?", + "removeFolders": "您確定要從工作區移除下列資料夾?", + "trashTitle": "移動{0} 到垃圾桶", + "trustEmptyWindow": "控制預設是否信任空工作區。", + "trustEnabled": "控制是否啟用工作區信任。如果停用,則信任所有工作區。", + "trustRequest": "擴充套件要求信任工作區,但對應的 API 尚未完全支援。您要信任此工作區嗎?", + "untitled-cleanup": "似乎有許多無標題的工作區檔案。請檢查{0} 並移除任何未使用的檔案。", + "workspaceFolderAdded": "已建立具有多個根的工作區。您是否要將工作區設定儲存為檔案?", + "workspaceFolderAddedTitle": "資料夾新增至工作區" + } + } +} diff --git a/packages/core/package.json b/packages/core/package.json index ee08a56e24377..33de3749014ac 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,11 +1,12 @@ { "name": "@theia/core", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia is a cloud & desktop IDE framework implemented in TypeScript.", "main": "lib/common/index.js", "typings": "lib/common/index.d.ts", "dependencies": { "@babel/runtime": "^7.10.0", + "@parcel/watcher": "^2.4.1", "@phosphor/algorithm": "1", "@phosphor/commands": "1", "@phosphor/coreutils": "1", @@ -16,12 +17,12 @@ "@phosphor/signaling": "1", "@phosphor/virtualdom": "1", "@phosphor/widgets": "1", - "@theia/application-package": "1.44.0", - "@theia/request": "1.44.0", + "@theia/application-package": "1.54.0", + "@theia/request": "1.54.0", "@types/body-parser": "^1.16.4", "@types/cookie": "^0.3.3", "@types/dompurify": "^2.2.2", - "@types/express": "^4.16.0", + "@types/express": "^4.17.21", "@types/fs-extra": "^4.0.2", "@types/lodash.debounce": "4.0.3", "@types/lodash.throttle": "^4.1.3", @@ -30,6 +31,7 @@ "@types/react-dom": "^18.0.6", "@types/route-parser": "^0.1.1", "@types/safer-buffer": "^2.1.0", + "@types/uuid": "^9.0.8", "@types/ws": "^8.5.5", "@types/yargs": "^15", "@vscode/codicons": "*", @@ -40,7 +42,7 @@ "dompurify": "^2.2.9", "drivelist": "^9.0.2", "es6-promise": "^4.2.4", - "express": "^4.16.3", + "express": "^4.21.0", "fast-json-stable-stringify": "^2.1.0", "file-icons-js": "~1.0.3", "font-awesome": "^4.7.0", @@ -55,8 +57,7 @@ "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "markdown-it": "^12.3.2", - "msgpackr": "1.6.1", - "nsfw": "^2.2.4", + "msgpackr": "^1.10.2", "p-debounce": "^2.1.0", "perfect-scrollbar": "^1.3.0", "react": "^18.2.0", @@ -68,10 +69,11 @@ "safer-buffer": "^2.1.2", "socket.io": "^4.5.3", "socket.io-client": "^4.5.3", - "uuid": "^8.3.2", + "tslib": "^2.6.2", + "uuid": "^9.0.1", "vscode-languageserver-protocol": "^3.17.2", "vscode-uri": "^2.1.1", - "ws": "^8.14.1", + "ws": "^8.17.1", "yargs": "^15.3.1" }, "peerDependencies": { @@ -117,11 +119,11 @@ "vscode-uri" ], "export =": [ + "@parcel/watcher as parcelWatcher", "dompurify as DOMPurify", "express", "lodash.debounce as debounce", "lodash.throttle as throttle", - "nsfw", "markdown-it as markdownit", "react as React", "ws as WebSocket", @@ -134,8 +136,12 @@ "frontendPreload": "lib/browser/preload/preload-module", "preload": "lib/electron-browser/preload" }, + { + "frontendOnlyPreload": "lib/browser-only/preload/frontend-only-preload-module" + }, { "frontend": "lib/browser/i18n/i18n-frontend-module", + "frontendOnly": "lib/browser-only/i18n/i18n-frontend-only-module", "backend": "lib/node/i18n/i18n-backend-module" }, { @@ -146,6 +152,9 @@ "frontend": "lib/browser/window/browser-window-module", "frontendElectron": "lib/electron-browser/window/electron-window-module" }, + { + "backendElectron": "lib/electron-node/cli/electron-backend-cli-module" + }, { "frontend": "lib/browser/keyboard/browser-keyboard-module", "frontendElectron": "lib/electron-browser/keyboard/electron-keyboard-module", @@ -194,15 +203,17 @@ "generate-layout": "electron ./scripts/generate-layout", "generate-theia-re-exports": "theia-re-exports generate && theia-re-exports template README_TEMPLATE.md > README.md", "lint": "theiaext lint", - "prepare": "yarn -s generate-theia-re-exports", + "prepare": "yarn -s generate-theia-re-exports && yarn download:json-schema", + "download:json-schema": "node ./scripts/download-catalog.js", "test": "theiaext test", "version": "yarn -s generate-theia-re-exports", "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0", - "@theia/re-exports": "1.44.0", - "minimist": "^1.2.0" + "@theia/ext-scripts": "1.54.0", + "@theia/re-exports": "1.54.0", + "minimist": "^1.2.0", + "nodejs-file-downloader": "4.13.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/core/scripts/download-catalog.js b/packages/core/scripts/download-catalog.js new file mode 100644 index 0000000000000..50fc84a8d6f2d --- /dev/null +++ b/packages/core/scripts/download-catalog.js @@ -0,0 +1,31 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +const { Downloader } = require('nodejs-file-downloader'); + +new Downloader({ + url: 'https://schemastore.org/api/json/catalog.json', + directory: './lib/browser', + fileName: 'catalog.json', + timeout: 60000, + proxy: process.env.http_proxy + || process.env.HTTP_PROXY + || process.env.https_proxy + || process.env.HTTPS_PROXY + || '', + cloneFiles: false +}).download(); + diff --git a/packages/core/shared/@parcel/watcher/index.d.ts b/packages/core/shared/@parcel/watcher/index.d.ts new file mode 100644 index 0000000000000..70c4fecbff0ae --- /dev/null +++ b/packages/core/shared/@parcel/watcher/index.d.ts @@ -0,0 +1,2 @@ +import parcelWatcher = require('@parcel/watcher'); +export = parcelWatcher; diff --git a/packages/core/shared/@parcel/watcher/index.js b/packages/core/shared/@parcel/watcher/index.js new file mode 100644 index 0000000000000..cc9f177038079 --- /dev/null +++ b/packages/core/shared/@parcel/watcher/index.js @@ -0,0 +1 @@ +module.exports = require('@parcel/watcher'); diff --git a/packages/core/shared/nsfw/index.d.ts b/packages/core/shared/nsfw/index.d.ts deleted file mode 100644 index d354af1682797..0000000000000 --- a/packages/core/shared/nsfw/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import nsfw = require('nsfw'); -export = nsfw; diff --git a/packages/core/shared/nsfw/index.js b/packages/core/shared/nsfw/index.js deleted file mode 100644 index fd3d1ddea09d5..0000000000000 --- a/packages/core/shared/nsfw/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('nsfw'); diff --git a/packages/core/src/browser-only/frontend-only-application-module.ts b/packages/core/src/browser-only/frontend-only-application-module.ts new file mode 100644 index 0000000000000..407f29297fc17 --- /dev/null +++ b/packages/core/src/browser-only/frontend-only-application-module.ts @@ -0,0 +1,116 @@ +// ***************************************************************************** +// 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 } from 'inversify'; +import { BackendStopwatch, CommandRegistry, Emitter, MeasurementOptions, OS } from '../common'; +import { ApplicationInfo, ApplicationServer, ExtensionInfo } from '../common/application-protocol'; +import { EnvVariable, EnvVariablesServer } from './../common/env-variables'; +import { bindMessageService } from '../browser/frontend-application-bindings'; +import { KeyStoreService } from '../common/key-store'; +import { QuickPickService } from '../common/quick-pick-service'; +import { QuickPickServiceImpl } from '../browser/quick-input'; +import { BackendRequestService, RequestService } from '@theia/request'; +import { ConnectionStatus, ConnectionStatusService } from '../browser/connection-status-service'; + +export { bindMessageService }; + +// is loaded directly after the regular frontend module +export const frontendOnlyApplicationModule = new ContainerModule((bind, unbind, isBound, rebind) => { + + if (isBound(CommandRegistry)) { + rebind(CommandRegistry).toSelf().inSingletonScope(); + } else { + bind(CommandRegistry).toSelf().inSingletonScope(); + } + + const stopwatch: BackendStopwatch = { + start: async (_name: string, _options?: MeasurementOptions | undefined): Promise => -1, + stop: async (_measurement: number, _message: string, _messageArgs: unknown[]): Promise => { } + }; + if (isBound(BackendStopwatch)) { + rebind(BackendStopwatch).toConstantValue(stopwatch); + } else { + bind(BackendStopwatch).toConstantValue(stopwatch); + } + + if (isBound(CommandRegistry)) { + rebind(QuickPickService).to(QuickPickServiceImpl).inSingletonScope(); + } else { + bind(QuickPickService).to(QuickPickServiceImpl).inSingletonScope(); + } + + const mockedApplicationServer: ApplicationServer = { + getExtensionsInfos: async (): Promise => [], + getApplicationInfo: async (): Promise => undefined, + getApplicationRoot: async (): Promise => '', + getApplicationPlatform: () => Promise.resolve('web'), + getBackendOS: async (): Promise => OS.Type.Linux + }; + if (isBound(ApplicationServer)) { + rebind(ApplicationServer).toConstantValue(mockedApplicationServer); + } else { + bind(ApplicationServer).toConstantValue(mockedApplicationServer); + } + + const varServer: EnvVariablesServer = { + getExecPath: async (): Promise => '', + getVariables: async (): Promise => [], + getValue: async (_key: string): Promise => undefined, + getConfigDirUri: async (): Promise => '', + getHomeDirUri: async (): Promise => '', + getDrives: async (): Promise => [] + }; + if (isBound(EnvVariablesServer)) { + rebind(EnvVariablesServer).toConstantValue(varServer); + } else { + bind(EnvVariablesServer).toConstantValue(varServer); + } + + const keyStoreService: KeyStoreService = { + deletePassword: () => Promise.resolve(false), + findCredentials: () => Promise.resolve([]), + findPassword: () => Promise.resolve(undefined), + setPassword: () => Promise.resolve(), + getPassword: () => Promise.resolve(undefined) + }; + if (isBound(KeyStoreService)) { + rebind(KeyStoreService).toConstantValue(keyStoreService); + } else { + bind(KeyStoreService).toConstantValue(keyStoreService); + } + + const requestService: RequestService = { + configure: () => Promise.resolve(), + request: () => Promise.reject(), + resolveProxy: () => Promise.resolve(undefined) + }; + if (isBound(BackendRequestService)) { + rebind(BackendRequestService).toConstantValue(requestService); + } else { + bind(BackendRequestService).toConstantValue(requestService); + } + + const connectionStatusService: ConnectionStatusService = { + currentStatus: ConnectionStatus.ONLINE, + onStatusChange: new Emitter().event + }; + if (isBound(ConnectionStatusService)) { + rebind(ConnectionStatusService).toConstantValue(connectionStatusService); + } else { + bind(ConnectionStatusService).toConstantValue(connectionStatusService); + } + +}); diff --git a/packages/core/src/browser-only/i18n/i18n-frontend-only-module.ts b/packages/core/src/browser-only/i18n/i18n-frontend-only-module.ts new file mode 100644 index 0000000000000..e88337fcb65b7 --- /dev/null +++ b/packages/core/src/browser-only/i18n/i18n-frontend-only-module.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// 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 } from 'inversify'; +import { AsyncLocalizationProvider, LanguageInfo, Localization } from '../../common/i18n/localization'; +import { LanguageQuickPickService } from '../../browser/i18n/language-quick-pick-service'; + +export default new ContainerModule(bind => { + const i18nMock: AsyncLocalizationProvider = { + getCurrentLanguage: async (): Promise => 'en', + setCurrentLanguage: async (_languageId: string): Promise => { + + }, + getAvailableLanguages: async (): Promise => + [] + , + loadLocalization: async (_languageId: string): Promise => ({ + translations: {}, + languageId: 'en' + }) + }; + bind(AsyncLocalizationProvider).toConstantValue(i18nMock); + bind(LanguageQuickPickService).toSelf().inSingletonScope(); +}); diff --git a/packages/core/src/browser-only/logger-frontend-only-module.ts b/packages/core/src/browser-only/logger-frontend-only-module.ts new file mode 100644 index 0000000000000..e5fc84021476d --- /dev/null +++ b/packages/core/src/browser-only/logger-frontend-only-module.ts @@ -0,0 +1,63 @@ +// ***************************************************************************** +// 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, Container } from 'inversify'; +import { ILoggerServer, ILoggerClient, LogLevel, ConsoleLogger } from '../common/logger-protocol'; +import { ILogger, Logger, LoggerFactory, LoggerName } from '../common/logger'; + +// is loaded directly after the regular logger frontend module +export const loggerFrontendOnlyModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const logger: ILoggerServer = { + setLogLevel: async (_name: string, _logLevel: number): Promise => { }, + getLogLevel: async (_name: string): Promise => LogLevel.INFO, + log: async (name: string, logLevel: number, message: string, params: unknown[]): Promise => { + ConsoleLogger.log(name, logLevel, message, params); + + }, + child: async (_name: string): Promise => { }, + dispose: (): void => { + }, + setClient: (_client: ILoggerClient | undefined): void => { + } + }; + if (isBound(ILoggerServer)) { + rebind(ILoggerServer).toConstantValue(logger); + } else { + bind(ILoggerServer).toConstantValue(logger); + } + + if (isBound(ILoggerServer)) { + rebind(LoggerFactory).toFactory(ctx => + (name: string) => { + const child = new Container({ defaultScope: 'Singleton' }); + child.parent = ctx.container; + child.bind(ILogger).to(Logger).inTransientScope(); + child.bind(LoggerName).toConstantValue(name); + return child.get(ILogger); + } + ); + } else { + bind(LoggerFactory).toFactory(ctx => + (name: string) => { + const child = new Container({ defaultScope: 'Singleton' }); + child.parent = ctx.container; + child.bind(ILogger).to(Logger).inTransientScope(); + child.bind(LoggerName).toConstantValue(name); + return child.get(ILogger); + } + ); + } +}); diff --git a/packages/core/src/browser-only/messaging/frontend-only-service-connection-provider.ts b/packages/core/src/browser-only/messaging/frontend-only-service-connection-provider.ts new file mode 100644 index 0000000000000..e2208565be4eb --- /dev/null +++ b/packages/core/src/browser-only/messaging/frontend-only-service-connection-provider.ts @@ -0,0 +1,39 @@ +// ***************************************************************************** +// 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 { Event, RpcProxy, Channel, RpcProxyFactory, Emitter } from '../../common'; +import { injectable } from 'inversify'; +import { ServiceConnectionProvider } from '../../browser/messaging/service-connection-provider'; +import { ConnectionSource } from '../../browser/messaging/connection-source'; + +@injectable() +export class FrontendOnlyConnectionSource implements ConnectionSource { + onConnectionDidOpen = new Emitter().event; +} + +@injectable() +export class FrontendOnlyServiceConnectionProvider extends ServiceConnectionProvider { + onSocketDidOpen = Event.None; + onSocketDidClose = Event.None; + onIncomingMessageActivity = Event.None; + override createProxy(path: unknown, target?: unknown): RpcProxy { + console.debug(`[Frontend-Only Fallback] Created proxy connection for ${path}`); + const factory = target instanceof RpcProxyFactory ? target : new RpcProxyFactory(target); + return factory.createProxy(); + } + override listen(path: string, handler: ServiceConnectionProvider.ConnectionHandler, reconnect: boolean): void { + console.debug('[Frontend-Only Fallback] Listen to websocket connection requested'); + } +} diff --git a/packages/core/src/browser-only/messaging/messaging-frontend-only-module.ts b/packages/core/src/browser-only/messaging/messaging-frontend-only-module.ts new file mode 100644 index 0000000000000..55029105c81d8 --- /dev/null +++ b/packages/core/src/browser-only/messaging/messaging-frontend-only-module.ts @@ -0,0 +1,42 @@ +// ***************************************************************************** +// 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 } from 'inversify'; +import { WebSocketConnectionSource } from '../../browser/messaging/ws-connection-source'; +import { FrontendOnlyConnectionSource, FrontendOnlyServiceConnectionProvider } from './frontend-only-service-connection-provider'; +import { ConnectionSource } from '../../browser/messaging/connection-source'; +import { LocalConnectionProvider, RemoteConnectionProvider } from '../../browser/messaging/service-connection-provider'; + +// is loaded directly after the regular message frontend module +export const messagingFrontendOnlyModule = new ContainerModule((bind, unbind, isBound, rebind) => { + unbind(WebSocketConnectionSource); + bind(FrontendOnlyConnectionSource).toSelf().inSingletonScope(); + if (isBound(ConnectionSource)) { + rebind(ConnectionSource).toService(FrontendOnlyConnectionSource); + } else { + bind(ConnectionSource).toService(FrontendOnlyConnectionSource); + } + bind(FrontendOnlyServiceConnectionProvider).toSelf().inSingletonScope(); + if (isBound(LocalConnectionProvider)) { + rebind(LocalConnectionProvider).toService(FrontendOnlyServiceConnectionProvider); + } else { + bind(LocalConnectionProvider).toService(FrontendOnlyServiceConnectionProvider); + } + if (isBound(RemoteConnectionProvider)) { + rebind(RemoteConnectionProvider).toService(FrontendOnlyServiceConnectionProvider); + } else { + bind(RemoteConnectionProvider).toService(FrontendOnlyServiceConnectionProvider); + } +}); diff --git a/packages/core/src/browser-only/preload/frontend-only-preload-module.ts b/packages/core/src/browser-only/preload/frontend-only-preload-module.ts new file mode 100644 index 0000000000000..ea0ed3fcb7e2a --- /dev/null +++ b/packages/core/src/browser-only/preload/frontend-only-preload-module.ts @@ -0,0 +1,49 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { ContainerModule } from 'inversify'; +import { LocalizationServer } from '../../common/i18n/localization-server'; +import { OS, OSBackendProvider } from '../../common/os'; +import { Localization } from '../../common/i18n/localization'; + +// loaded after regular preload module +export default new ContainerModule((bind, unbind, isBound, rebind) => { + const frontendOnlyLocalizationServer: LocalizationServer = { + loadLocalization: async (languageId: string): Promise => ({ translations: {}, languageId }) + }; + if (isBound(LocalizationServer)) { + rebind(LocalizationServer).toConstantValue(frontendOnlyLocalizationServer); + } else { + bind(LocalizationServer).toConstantValue(frontendOnlyLocalizationServer); + } + + const frontendOnlyOSBackendProvider: OSBackendProvider = { + getBackendOS: async (): Promise => { + if (window.navigator.platform.startsWith('Win')) { + return OS.Type.Windows; + } else if (window.navigator.platform.startsWith('Mac')) { + return OS.Type.OSX; + } else { + return OS.Type.Linux; + } + } + }; + if (isBound(OSBackendProvider)) { + rebind(OSBackendProvider).toConstantValue(frontendOnlyOSBackendProvider); + } else { + bind(OSBackendProvider).toConstantValue(frontendOnlyOSBackendProvider); + } +}); diff --git a/packages/core/src/browser/authentication-service.ts b/packages/core/src/browser/authentication-service.ts index e9595b816248f..fa06485e39600 100644 --- a/packages/core/src/browser/authentication-service.ts +++ b/packages/core/src/browser/authentication-service.ts @@ -32,6 +32,13 @@ export interface AuthenticationSessionAccountInformation { readonly id: string; readonly label: string; } +export interface AuthenticationProviderSessionOptions { + /** + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. + */ + account?: AuthenticationSessionAccountInformation; +} export interface AuthenticationSession { id: string; @@ -82,16 +89,6 @@ export interface AuthenticationProvider { updateSessionItems(event: AuthenticationProviderAuthenticationSessionsChangeEvent): Promise; - /** - * @deprecated use `createSession` instead. - */ - login(scopes: string[]): Promise; - - /** - * @deprecated use `removeSession` instead. - */ - logout(sessionId: string): Promise; - /** * An [event](#Event) which fires when the array of sessions has changed, or data * within a session has changed. @@ -102,16 +99,18 @@ export interface AuthenticationProvider { * Get a list of sessions. * @param scopes An optional list of scopes. If provided, the sessions returned should match * these permissions, otherwise all sessions should be returned. + * @param account The optional account that you would like to get the session for * @returns A promise that resolves to an array of authentication sessions. */ - getSessions(scopes?: string[]): Thenable>; + getSessions(scopes: string[] | undefined, account?: AuthenticationSessionAccountInformation): Thenable>; /** * Prompts a user to login. * @param scopes A list of scopes, permissions, that the new session should be created with. + * @param options The options for createing the session * @returns A promise that resolves to an authentication session. */ - createSession(scopes: string[]): Thenable; + createSession(scopes: string[], options: AuthenticationProviderSessionOptions): Thenable; /** * Removes the session corresponding to session id. @@ -133,10 +132,11 @@ export interface AuthenticationService { readonly onDidUnregisterAuthenticationProvider: Event; readonly onDidChangeSessions: Event<{ providerId: string, label: string, event: AuthenticationProviderAuthenticationSessionsChangeEvent }>; - getSessions(providerId: string, scopes?: string[]): Promise>; + readonly onDidUpdateSignInCount: Event; + getSessions(providerId: string, scopes?: string[], user?: AuthenticationSessionAccountInformation): Promise>; getLabel(providerId: string): string; supportsMultipleAccounts(providerId: string): boolean; - login(providerId: string, scopes: string[]): Promise; + login(providerId: string, scopes: string[], options?: AuthenticationProviderSessionOptions): Promise; logout(providerId: string, sessionId: string): Promise; signOutOfAccount(providerId: string, accountName: string): Promise; @@ -157,15 +157,18 @@ export class AuthenticationServiceImpl implements AuthenticationService { protected authenticationProviders: Map = new Map(); - private onDidRegisterAuthenticationProviderEmitter: Emitter = new Emitter(); + private readonly onDidRegisterAuthenticationProviderEmitter: Emitter = new Emitter(); readonly onDidRegisterAuthenticationProvider: Event = this.onDidRegisterAuthenticationProviderEmitter.event; - private onDidUnregisterAuthenticationProviderEmitter: Emitter = new Emitter(); + private readonly onDidUnregisterAuthenticationProviderEmitter: Emitter = new Emitter(); readonly onDidUnregisterAuthenticationProvider: Event = this.onDidUnregisterAuthenticationProviderEmitter.event; - private onDidChangeSessionsEmitter: Emitter = new Emitter(); + private readonly onDidChangeSessionsEmitter: Emitter = new Emitter(); readonly onDidChangeSessions: Event = this.onDidChangeSessionsEmitter.event; + private readonly onDidChangeSignInCountEmitter: Emitter = new Emitter(); + readonly onDidUpdateSignInCount: Event = this.onDidChangeSignInCountEmitter.event; + @inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry; @inject(CommandRegistry) protected readonly commands: CommandRegistry; @inject(StorageService) protected readonly storageService: StorageService; @@ -295,7 +298,8 @@ export class AuthenticationServiceImpl implements AuthenticationService { return; } - const sessions = await provider.getSessions(); + const previousSize = this.signInRequestItems.size; + const sessions = await provider.getSessions(undefined); Object.keys(existingRequestsForProvider).forEach(requestedScopes => { if (sessions.some(session => session.scopes.slice().sort().join('') === requestedScopes)) { const sessionRequest = existingRequestsForProvider[requestedScopes]; @@ -311,6 +315,9 @@ export class AuthenticationServiceImpl implements AuthenticationService { } } }); + if (previousSize !== this.signInRequestItems.size) { + this.onDidChangeSignInCountEmitter.fire(this.signInRequestItems.size); + } } async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { @@ -341,7 +348,7 @@ export class AuthenticationServiceImpl implements AuthenticationService { } const menuItem = this.menus.registerMenuAction(ACCOUNTS_SUBMENU, { - label: `Sign in to use ${extensionName} (1)`, + label: nls.localizeByDefault('Sign in with {0} to use {1} (1)', provider.label, extensionName), order: '1', commandId: `${extensionId}signIn`, }); @@ -362,6 +369,7 @@ export class AuthenticationServiceImpl implements AuthenticationService { } }); + const previousSize = this.signInRequestItems.size; if (providerRequests) { const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] }; @@ -378,6 +386,9 @@ export class AuthenticationServiceImpl implements AuthenticationService { } }); } + if (previousSize !== this.signInRequestItems.size) { + this.onDidChangeSignInCountEmitter.fire(this.signInRequestItems.size); + } } } @@ -399,19 +410,19 @@ export class AuthenticationServiceImpl implements AuthenticationService { } } - async getSessions(id: string, scopes?: string[]): Promise> { + async getSessions(id: string, scopes?: string[], user?: AuthenticationSessionAccountInformation): Promise> { const authProvider = this.authenticationProviders.get(id); if (authProvider) { - return authProvider.getSessions(scopes); + return authProvider.getSessions(scopes, user); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } } - async login(id: string, scopes: string[]): Promise { + async login(id: string, scopes: string[], options?: AuthenticationProviderSessionOptions): Promise { const authProvider = this.authenticationProviders.get(id); if (authProvider) { - return authProvider.createSession(scopes); + return authProvider.createSession(scopes, options || {}); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } diff --git a/packages/core/src/browser/browser.ts b/packages/core/src/browser/browser.ts index eace2b5f2670d..5cab4f39631ad 100644 --- a/packages/core/src/browser/browser.ts +++ b/packages/core/src/browser/browser.ts @@ -18,6 +18,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Disposable, environment } from '../common'; + const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : ''; export const isIE = (userAgent.indexOf('Trident') >= 0); @@ -31,7 +33,15 @@ export const isChrome = (userAgent.indexOf('Chrome') >= 0); export const isSafari = (userAgent.indexOf('Chrome') === -1) && (userAgent.indexOf('Safari') >= 0); export const isIPad = (userAgent.indexOf('iPad') >= 0); // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isNative = typeof (window as any).process !== 'undefined'; +/** + * @deprecated use Environment.electron.is + */ +export const isNative = environment.electron.is(); +/** + * Determines whether the backend is running in a remote environment. + * I.e. we use the browser version or connect to a remote Theia instance in Electron. + */ +export const isRemote = !environment.electron.is() || new URL(location.href).searchParams.has('localPort'); // eslint-disable-next-line @typescript-eslint/no-explicit-any export const isBasicWasmSupported = typeof (window as any).WebAssembly !== 'undefined'; @@ -218,3 +228,12 @@ function getMeasurementElement(style?: PartialCSSStyle): HTMLElement { } return measureElement; } + +export function onDomEvent( + element: Node, + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => unknown, + options?: boolean | AddEventListenerOptions): Disposable { + element.addEventListener(type, listener, options); + return { dispose: () => element.removeEventListener(type, listener, options) }; +} diff --git a/packages/core/src/browser/color-application-contribution.ts b/packages/core/src/browser/color-application-contribution.ts index 4ad35b3de44b5..91cb42774fbdd 100644 --- a/packages/core/src/browser/color-application-contribution.ts +++ b/packages/core/src/browser/color-application-contribution.ts @@ -22,6 +22,7 @@ import { FrontendApplicationContribution } from './frontend-application-contribu import { ContributionProvider } from '../common/contribution-provider'; import { Disposable, DisposableCollection } from '../common/disposable'; import { DEFAULT_BACKGROUND_COLOR_STORAGE_KEY } from './frontend-application-config-provider'; +import { SecondaryWindowHandler } from './secondary-window-handler'; export const ColorContribution = Symbol('ColorContribution'); export interface ColorContribution { @@ -43,6 +44,9 @@ export class ColorApplicationContribution implements FrontendApplicationContribu @inject(ThemeService) protected readonly themeService: ThemeService; + @inject(SecondaryWindowHandler) + protected readonly secondaryWindowHandler: SecondaryWindowHandler; + onStart(): void { for (const contribution of this.colorContributions.getContributions()) { contribution.registerColors(this.colors); @@ -55,13 +59,18 @@ export class ColorApplicationContribution implements FrontendApplicationContribu this.colors.onDidChange(() => this.update()); this.registerWindow(window); + this.secondaryWindowHandler.onWillAddWidget(([widget, window]) => { + this.registerWindow(window); + }); + this.secondaryWindowHandler.onWillRemoveWidget(([widget, window]) => { + this.windows.delete(window); + }); } - registerWindow(win: Window): Disposable { + registerWindow(win: Window): void { this.windows.add(win); this.updateWindow(win); this.onDidChangeEmitter.fire(); - return Disposable.create(() => this.windows.delete(win)); } protected readonly toUpdate = new DisposableCollection(); diff --git a/packages/core/src/browser/command-open-handler.ts b/packages/core/src/browser/command-open-handler.ts index f3e7323b04c07..2692ae5347765 100644 --- a/packages/core/src/browser/command-open-handler.ts +++ b/packages/core/src/browser/command-open-handler.ts @@ -41,7 +41,7 @@ export class CommandOpenHandler implements OpenHandler { try { args = JSON.parse(uri.query); } catch { - // ignore error + args = uri.query; } } if (!Array.isArray(args)) { diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index f436fcf2349b4..64489486e30f0 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -57,15 +57,17 @@ import { QuickInputService, QuickPickItem, QuickPickItemOrSeparator, QuickPickSe import { AsyncLocalizationProvider } from '../common/i18n/localization'; import { nls } from '../common/nls'; import { CurrentWidgetCommandAdapter } from './shell/current-widget-command-adapter'; -import { ConfirmDialog, confirmExitWithOrWithoutSaving, Dialog } from './dialogs'; +import { ConfirmDialog, confirmExit, ConfirmSaveDialog, Dialog } from './dialogs'; import { WindowService } from './window/window-service'; import { FrontendApplicationConfigProvider } from './frontend-application-config-provider'; import { DecorationStyle } from './decoration-style'; -import { isPinned, Title, togglePinned, Widget } from './widgets'; -import { SaveResourceService } from './save-resource-service'; +import { codicon, isPinned, Title, togglePinned, Widget } from './widgets'; +import { SaveableService } from './saveable-service'; import { UserWorkingDirectoryProvider } from './user-working-directory-provider'; import { UNTITLED_SCHEME, UntitledResourceResolver } from '../common'; import { LanguageQuickPickService } from './i18n/language-quick-pick-service'; +import { SidebarMenu } from './shell/sidebar-menu-widget'; +import { UndoRedoHandlerService } from './undo-redo-handler'; export namespace CommonMenus { @@ -280,6 +282,15 @@ export namespace CommonCommands { category: VIEW_CATEGORY, label: 'Toggle Menu Bar' }); + /** + * Command Parameters: + * - `fileName`: string + * - `directory`: URI + */ + export const NEW_FILE = Command.toDefaultLocalizedCommand({ + id: 'workbench.action.files.newFile', + category: FILE_CATEGORY + }); export const NEW_UNTITLED_TEXT_FILE = Command.toDefaultLocalizedCommand({ id: 'workbench.action.files.newUntitledTextFile', category: FILE_CATEGORY, @@ -351,12 +362,12 @@ export namespace CommonCommands { }); } -export const supportCut = browser.isNative || document.queryCommandSupported('cut'); -export const supportCopy = browser.isNative || document.queryCommandSupported('copy'); +export const supportCut = environment.electron.is() || document.queryCommandSupported('cut'); +export const supportCopy = environment.electron.is() || document.queryCommandSupported('copy'); // Chrome incorrectly returns true for document.queryCommandSupported('paste') // when the paste feature is available but the calling script has insufficient // privileges to actually perform the action -export const supportPaste = browser.isNative || (!browser.isChrome && document.queryCommandSupported('paste')); +export const supportPaste = environment.electron.is() || (!browser.isChrome && document.queryCommandSupported('paste')); export const RECENT_COMMANDS_STORAGE_KEY = 'commands'; @@ -376,7 +387,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi @inject(OpenerService) protected readonly openerService: OpenerService, @inject(AboutDialog) protected readonly aboutDialog: AboutDialog, @inject(AsyncLocalizationProvider) protected readonly localizationProvider: AsyncLocalizationProvider, - @inject(SaveResourceService) protected readonly saveResourceService: SaveResourceService, + @inject(SaveableService) protected readonly saveResourceService: SaveableService, ) { } @inject(ContextKeyService) @@ -433,7 +444,11 @@ export class CommonFrontendContribution implements FrontendApplicationContributi @inject(UntitledResourceResolver) protected readonly untitledResourceResolver: UntitledResourceResolver; + @inject(UndoRedoHandlerService) + protected readonly undoRedoHandlerService: UndoRedoHandlerService; + protected pinnedKey: ContextKey; + protected inputFocus: ContextKey; async configure(app: FrontendApplication): Promise { // FIXME: This request blocks valuable startup time (~200ms). @@ -448,6 +463,9 @@ export class CommonFrontendContribution implements FrontendApplicationContributi this.contextKeyService.createKey('isMac', OS.type() === OS.Type.OSX); this.contextKeyService.createKey('isWindows', OS.type() === OS.Type.Windows); this.contextKeyService.createKey('isWeb', !this.isElectron()); + this.inputFocus = this.contextKeyService.createKey('inputFocus', false); + this.updateInputFocus(); + browser.onDomEvent(document, 'focusin', () => this.updateInputFocus()); this.pinnedKey = this.contextKeyService.createKey('activeEditorIsPinned', false); this.updatePinnedKey(); @@ -463,17 +481,18 @@ export class CommonFrontendContribution implements FrontendApplicationContributi app.shell.leftPanelHandler.addBottomMenu({ id: 'settings-menu', - iconClass: 'codicon codicon-settings-gear', + iconClass: codicon('settings-gear'), title: nls.localizeByDefault(CommonCommands.MANAGE_CATEGORY), menuPath: MANAGE_MENU, - order: 1, + order: 0, }); - const accountsMenu = { + const accountsMenu: SidebarMenu = { id: 'accounts-menu', - iconClass: 'codicon codicon-person', + iconClass: codicon('account'), title: nls.localizeByDefault('Accounts'), menuPath: ACCOUNTS_MENU, - order: 0, + order: 1, + onDidBadgeChange: this.authenticationService.onDidUpdateSignInCount }; this.authenticationService.onDidRegisterAuthenticationProvider(() => { app.shell.leftPanelHandler.addBottomMenu(accountsMenu); @@ -502,6 +521,15 @@ export class CommonFrontendContribution implements FrontendApplicationContributi } } + protected updateInputFocus(): void { + const activeElement = document.activeElement; + if (activeElement) { + const isInput = activeElement.tagName?.toLowerCase() === 'input' + || activeElement.tagName?.toLowerCase() === 'textarea'; + this.inputFocus.set(isInput); + } + } + protected updatePinnedKey(): void { const activeTab = this.shell.findTabBar(); const pinningTarget = activeTab && this.shell.findTitle(activeTab); @@ -521,7 +549,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi if (newValue === 'compact') { this.shell.leftPanelHandler.addTopMenu({ id: mainMenuId, - iconClass: 'codicon codicon-menu', + iconClass: `theia-compact-menu ${codicon('menu')}`, title: nls.localizeByDefault('Application Menu'), menuPath: MAIN_MENU_BAR, order: 0, @@ -790,10 +818,14 @@ export class CommonFrontendContribution implements FrontendApplicationContributi })); commandRegistry.registerCommand(CommonCommands.UNDO, { - execute: () => document.execCommand('undo') + execute: () => { + this.undoRedoHandlerService.undo(); + } }); commandRegistry.registerCommand(CommonCommands.REDO, { - execute: () => document.execCommand('redo') + execute: () => { + this.undoRedoHandlerService.redo(); + } }); commandRegistry.registerCommand(CommonCommands.SELECT_ALL, { execute: () => document.execCommand('selectAll') @@ -1056,7 +1088,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi }, { command: CommonCommands.REDO.id, - keybinding: 'ctrlcmd+shift+z' + keybinding: isOSX ? 'ctrlcmd+shift+z' : 'ctrlcmd+y' }, { command: CommonCommands.SELECT_ALL.id, @@ -1200,22 +1232,60 @@ export class CommonFrontendContribution implements FrontendApplicationContributi action: async () => { const captionsToSave = this.unsavedTabsCaptions(); const untitledCaptionsToSave = this.unsavedUntitledTabsCaptions(); - const result = await confirmExitWithOrWithoutSaving(captionsToSave, async () => { + const shouldExit = await this.confirmExitWithOrWithoutSaving(captionsToSave, async () => { await this.saveDirty(untitledCaptionsToSave); await this.shell.saveAll(); }); - if (this.shell.canSaveAll()) { - this.shouldPreventClose = true; - return false; - } else { - this.shouldPreventClose = false; - return result; - } + const allSavedOrDoNotSave = ( + shouldExit === true && untitledCaptionsToSave.length === 0 // Should save and cancel if any captions failed to save + ) || shouldExit === false; // Do not save + + this.shouldPreventClose = !allSavedOrDoNotSave; + return allSavedOrDoNotSave; } }; } } + // Asks the user to confirm whether they want to exit with or without saving the changes + private async confirmExitWithOrWithoutSaving(captionsToSave: string[], performSave: () => Promise): Promise { + const div: HTMLElement = document.createElement('div'); + div.innerText = nls.localizeByDefault("Your changes will be lost if you don't save them."); + + let result; + if (captionsToSave.length > 0) { + const span = document.createElement('span'); + span.appendChild(document.createElement('br')); + captionsToSave.forEach(cap => { + const b = document.createElement('b'); + b.innerText = cap; + span.appendChild(b); + span.appendChild(document.createElement('br')); + }); + span.appendChild(document.createElement('br')); + div.appendChild(span); + result = await new ConfirmSaveDialog({ + title: nls.localizeByDefault('Do you want to save the changes to the following {0} files?', captionsToSave.length), + msg: div, + dontSave: nls.localizeByDefault("Don't Save"), + save: nls.localizeByDefault('Save All'), + cancel: Dialog.CANCEL + }).open(); + + if (result) { + await performSave(); + } + } else { + // fallback if not passed with an empty caption-list. + result = confirmExit(); + } + if (result !== undefined) { + return result === true; + } else { + return undefined; + }; + + } protected unsavedTabsCaptions(): string[] { return this.shell.widgets .filter(widget => this.saveResourceService.canSave(widget)) @@ -1236,11 +1306,19 @@ export class CommonFrontendContribution implements FrontendApplicationContributi this.windowService.reload(); } } + /** + * saves any dirty widget in toSave + * side effect - will pop all widgets from toSave that was saved + * @param toSave + */ protected async saveDirty(toSave: Widget[]): Promise { for (const widget of toSave) { const saveable = Saveable.get(widget); if (saveable?.dirty) { await this.saveResourceService.save(widget); + if (!this.saveResourceService.canSave(widget)) { + toSave.pop(); + } } } } @@ -1353,7 +1431,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi const items = [...itemsByTheme.light, ...itemsByTheme.dark, ...itemsByTheme.hc, ...itemsByTheme.hcLight]; this.quickInputService?.showQuickPick(items, { - placeholder: nls.localizeByDefault('Select Color Theme (Up/Down Keys to Preview)'), + placeholder: nls.localizeByDefault('Select Color Theme'), activeItem: items.find(item => item.id === resetTo), onDidChangeSelection: (_, selectedItems) => { resetTo = undefined; @@ -1378,6 +1456,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi const items: QuickPickItemOrSeparator[] = [ { label: nls.localizeByDefault('New Text File'), + description: nls.localizeByDefault('Built-in'), execute: async () => this.commandRegistry.executeCommand(CommonCommands.NEW_UNTITLED_TEXT_FILE.id) }, ...newFileContributions.children @@ -1400,10 +1479,43 @@ export class CommonFrontendContribution implements FrontendApplicationContributi }) ]; + + const CREATE_NEW_FILE_ITEM_ID = 'create-new-file'; + const hasNewFileHandler = this.commandRegistry.getActiveHandler(CommonCommands.NEW_FILE.id) !== undefined; + // Create a "Create New File" item only if there is a NEW_FILE command handler. + const createNewFileItem: QuickPickItem & { value?: string } | undefined = hasNewFileHandler ? { + id: CREATE_NEW_FILE_ITEM_ID, + label: nls.localizeByDefault('Create New File ({0})'), + description: nls.localizeByDefault('Built-in'), + execute: async () => { + if (createNewFileItem?.value) { + const parent = await this.workingDirProvider.getUserWorkingDir(); + // Exec NEW_FILE command with the file name and parent dir as arguments + return this.commandRegistry.executeCommand(CommonCommands.NEW_FILE.id, createNewFileItem.value, parent); + } + } + } : undefined; + this.quickInputService.showQuickPick(items, { title: nls.localizeByDefault('New File...'), placeholder: nls.localizeByDefault('Select File Type or Enter File Name...'), - canSelectMany: false + canSelectMany: false, + onDidChangeValue: picker => { + if (createNewFileItem === undefined) { + return; + } + // Dynamically show or hide the "Create New File" item based on the input value. + if (picker.value) { + createNewFileItem.alwaysShow = true; + createNewFileItem.value = picker.value; + createNewFileItem.label = nls.localizeByDefault('Create New File ({0})', picker.value); + picker.items = [...items, createNewFileItem]; + } else { + createNewFileItem.alwaysShow = false; + createNewFileItem.value = undefined; + picker.items = items.filter(item => item !== createNewFileItem); + } + } }); } @@ -1808,6 +1920,94 @@ export class CommonFrontendContribution implements FrontendApplicationContributi }, description: 'Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.' }, + // editor find + + { + id: 'editor.findMatchBackground', + defaults: { + light: '#A8AC94', + dark: '#515C6A', + hcDark: undefined, + hcLight: undefined + }, + description: 'Color of the current search match.' + }, + + { + id: 'editor.findMatchForeground', + defaults: { + light: undefined, + dark: undefined, + hcDark: undefined, + hcLight: undefined + }, + description: 'Text color of the current search match.' + }, + { + id: 'editor.findMatchHighlightBackground', + defaults: { + light: '#EA5C0055', + dark: '#EA5C0055', + hcDark: undefined, + hcLight: undefined + }, + description: 'Color of the other search matches. The color must not be opaque so as not to hide underlying decorations.' + }, + + { + id: 'editor.findMatchHighlightForeground', + defaults: { + light: undefined, + dark: undefined, + hcDark: undefined, + hcLight: undefined + }, + description: 'Foreground color of the other search matches.' + }, + + { + id: 'editor.findRangeHighlightBackground', + defaults: { + dark: '#3a3d4166', + light: '#b4b4b44d', + hcDark: undefined, + hcLight: undefined + }, + description: 'Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.' + }, + + { + id: 'editor.findMatchBorder', + defaults: { + light: undefined, + dark: undefined, + hcDark: 'activeContrastBorder', + hcLight: 'activeContrastBorder' + }, + description: 'Border color of the current search match.' + }, + { + id: 'editor.findMatchHighlightBorder', + defaults: { + light: undefined, + dark: undefined, + hcDark: 'activeContrastBorder', + hcLight: 'activeContrastBorder' + }, + description: 'Border color of the other search matches.' + }, + + { + id: 'editor.findRangeHighlightBorder', + defaults: { + dark: undefined, + light: undefined, + hcDark: Color.transparent('activeContrastBorder', 0.4), + hcLight: Color.transparent('activeContrastBorder', 0.4) + }, + description: 'Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.' + }, + // Quickinput colors should be aligned with https://code.visualstudio.com/api/references/theme-color#quick-picker // if not yet contributed by Monaco, check runtime css variables to learn. { diff --git a/packages/core/src/browser/connection-status-service.spec.ts b/packages/core/src/browser/connection-status-service.spec.ts index 2d570f0ec62f6..3e3d8fb45efc6 100644 --- a/packages/core/src/browser/connection-status-service.spec.ts +++ b/packages/core/src/browser/connection-status-service.spec.ts @@ -33,8 +33,8 @@ import { MockConnectionStatusService } from './test/mock-connection-status-servi import * as sinon from 'sinon'; import { Container } from 'inversify'; -import { WebSocketConnectionProvider } from './messaging/ws-connection-provider'; import { ILogger, Emitter, Loggable } from '../common'; +import { WebSocketConnectionSource } from './messaging/ws-connection-source'; disableJSDOM(); @@ -101,7 +101,7 @@ describe('frontend-connection-status', function (): void { let timer: sinon.SinonFakeTimers; let pingSpy: sinon.SinonSpy; beforeEach(() => { - const mockWebSocketConnectionProvider = sinon.createStubInstance(WebSocketConnectionProvider); + const mockWebSocketConnectionSource = sinon.createStubInstance(WebSocketConnectionSource); const mockPingService: PingService = { ping(): Promise { return Promise.resolve(undefined); @@ -118,11 +118,11 @@ describe('frontend-connection-status', function (): void { testContainer.bind(PingService).toConstantValue(mockPingService); testContainer.bind(ILogger).toConstantValue(mockILogger); testContainer.bind(ConnectionStatusOptions).toConstantValue({ offlineTimeout: OFFLINE_TIMEOUT }); - testContainer.bind(WebSocketConnectionProvider).toConstantValue(mockWebSocketConnectionProvider); + testContainer.bind(WebSocketConnectionSource).toConstantValue(mockWebSocketConnectionSource); - sinon.stub(mockWebSocketConnectionProvider, 'onSocketDidOpen').value(mockSocketOpenedEmitter.event); - sinon.stub(mockWebSocketConnectionProvider, 'onSocketDidClose').value(mockSocketClosedEmitter.event); - sinon.stub(mockWebSocketConnectionProvider, 'onIncomingMessageActivity').value(mockIncomingMessageActivityEmitter.event); + sinon.stub(mockWebSocketConnectionSource, 'onSocketDidOpen').value(mockSocketOpenedEmitter.event); + sinon.stub(mockWebSocketConnectionSource, 'onSocketDidClose').value(mockSocketClosedEmitter.event); + sinon.stub(mockWebSocketConnectionSource, 'onIncomingMessageActivity').value(mockIncomingMessageActivityEmitter.event); timer = sinon.useFakeTimers(); diff --git a/packages/core/src/browser/connection-status-service.ts b/packages/core/src/browser/connection-status-service.ts index 790a702480f00..0a4b58006dfe3 100644 --- a/packages/core/src/browser/connection-status-service.ts +++ b/packages/core/src/browser/connection-status-service.ts @@ -19,8 +19,8 @@ import { ILogger } from '../common/logger'; import { Event, Emitter } from '../common/event'; import { DefaultFrontendApplicationContribution } from './frontend-application-contribution'; import { StatusBar, StatusBarAlignment } from './status-bar/status-bar'; -import { WebSocketConnectionProvider } from './messaging/ws-connection-provider'; import { Disposable, DisposableCollection, nls } from '../common'; +import { WebSocketConnectionSource } from './messaging/ws-connection-source'; /** * Service for listening on backend connection changes. @@ -119,7 +119,7 @@ export class FrontendConnectionStatusService extends AbstractConnectionStatusSer private scheduledPing: number | undefined; - @inject(WebSocketConnectionProvider) protected readonly wsConnectionProvider: WebSocketConnectionProvider; + @inject(WebSocketConnectionSource) protected readonly wsConnectionProvider: WebSocketConnectionSource; @inject(PingService) protected readonly pingService: PingService; @postConstruct() diff --git a/packages/core/src/browser/context-key-service.ts b/packages/core/src/browser/context-key-service.ts index 9ed540beb5394..c250c18999948 100644 --- a/packages/core/src/browser/context-key-service.ts +++ b/packages/core/src/browser/context-key-service.ts @@ -15,8 +15,8 @@ // ***************************************************************************** import { injectable } from 'inversify'; -import { Disposable } from '../common'; import { Emitter, Event } from '../common/event'; +import { Disposable } from '../common'; export type ContextKeyValue = null | undefined | boolean | number | string | Array @@ -43,7 +43,7 @@ export interface ContextKeyChangeEvent { export const ContextKeyService = Symbol('ContextKeyService'); -export interface ContextMatcher extends Disposable { +export interface ContextMatcher { /** * Whether the expression is satisfied. If `context` provided, the service will attempt to retrieve a context object associated with that element. */ @@ -84,11 +84,10 @@ export interface ContextKeyService extends ContextMatcher { setContext(key: string, value: unknown): void; } -export type ScopedValueStore = Omit; +export type ScopedValueStore = Omit & Disposable; @injectable() export class ContextKeyServiceDummyImpl implements ContextKeyService { - protected readonly onDidChangeEmitter = new Emitter(); readonly onDidChange = this.onDidChangeEmitter.event; protected fireDidChange(event: ContextKeyChangeEvent): void { @@ -123,7 +122,7 @@ export class ContextKeyServiceDummyImpl implements ContextKeyService { /** * Details should implemented by an extension, e.g. by the monaco extension. */ - createScoped(target: HTMLElement): ContextKeyService { + createScoped(target: HTMLElement): ScopedValueStore { return this; } diff --git a/packages/core/src/browser/context-menu-renderer.ts b/packages/core/src/browser/context-menu-renderer.ts index e8f88586f7c35..dd9bad8463373 100644 --- a/packages/core/src/browser/context-menu-renderer.ts +++ b/packages/core/src/browser/context-menu-renderer.ts @@ -26,10 +26,6 @@ export const Coordinate = Symbol('Coordinate'); export type Anchor = MouseEvent | Coordinate; -export function toAnchor(anchor: HTMLElement | Coordinate): Anchor { - return anchor instanceof HTMLElement ? { x: anchor.offsetLeft, y: anchor.offsetTop } : anchor; -} - export function coordinateFromAnchor(anchor: Anchor): Coordinate { const { x, y } = anchor instanceof MouseEvent ? { x: anchor.clientX, y: anchor.clientY } : anchor; return { x, y }; diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts index 45ae47154a852..c9827c5aaf490 100644 --- a/packages/core/src/browser/core-preferences.ts +++ b/packages/core/src/browser/core-preferences.ts @@ -119,6 +119,22 @@ export const corePreferenceSchema: PreferenceSchema = { scope: 'application', markdownDescription: nls.localizeByDefault('Separator used by {0}.', '`#window.title#`') }, + 'window.secondaryWindowPlacement': { + type: 'string', + enum: ['originalSize', 'halfWidth', 'fullSize'], + enumDescriptions: [ + nls.localize('theia/core/secondaryWindow/originalSize', 'The position and size of the extracted widget will be the same as the original widget.'), + nls.localize('theia/core/secondaryWindow/halfWidth', 'The position and size of the extracted widget will be half the width of the running Theia application.'), + nls.localize('theia/core/secondaryWindow/fullSize', 'The position and size of the extracted widget will be the same as the running Theia application.'), + ], + default: 'originalSize', + description: nls.localize('theia/core/secondaryWindow/description', 'Sets the initial position and size of the extracted secondary window.'), + }, + 'window.secondaryWindowAlwaysOnTop': { + type: 'boolean', + default: false, + description: nls.localize('theia/core/secondaryWindow/alwaysOnTop', 'When enabled, the secondary window stays above all other windows, including those of different applications.'), + }, 'http.proxy': { type: 'string', pattern: '^https?://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$', @@ -184,6 +200,11 @@ export const corePreferenceSchema: PreferenceSchema = { 'description': nls.localizeByDefault('Controls whether an editor is revealed in any of the visible groups if opened. If disabled, an editor will prefer to open in the currently active editor group. If enabled, an already opened editor will be revealed instead of opened again in the currently active editor group. Note that there are some cases where this setting is ignored, such as when forcing an editor to open in a specific group or to the side of the currently active group.'), 'default': false }, + 'workbench.editor.decorations.badges': { + 'type': 'boolean', + 'description': nls.localizeByDefault('Controls whether editor file decorations should use badges.'), + 'default': true + }, 'workbench.commandPalette.history': { type: 'number', default: 50, @@ -195,7 +216,7 @@ export const corePreferenceSchema: PreferenceSchema = { enum: ['dark', 'light', 'hc-theia'], enumItemLabels: ['Dark (Theia)', 'Light (Theia)', 'High Contrast (Theia)'], default: DefaultTheme.defaultForOSTheme(FrontendApplicationConfigProvider.get().defaultTheme), - description: nls.localizeByDefault('Specifies the color theme used in the workbench.') + description: nls.localizeByDefault('Specifies the color theme used in the workbench when {0} is not enabled.', '`#window.autoDetectColorScheme#`') }, 'workbench.iconTheme': { type: ['string'], @@ -260,6 +281,15 @@ export const corePreferenceSchema: PreferenceSchema = { default: 200, minimum: 10, description: nls.localize('theia/core/tabDefaultSize', 'Specifies the default size for tabs.') + }, + 'workbench.editorAssociations': { + type: 'object', + markdownDescription: nls.localizeByDefault('Configure [glob patterns](https://aka.ms/vscode-glob-patterns) to editors (for example `"*.hex": "hexEditor.hexedit"`). These have precedence over the default behavior.'), + patternProperties: { + '.*': { + type: 'string' + } + } } } }; @@ -279,6 +309,7 @@ export interface CoreConfiguration { 'workbench.editor.mouseBackForwardToNavigate': boolean; 'workbench.editor.closeOnFileDelete': boolean; 'workbench.editor.revealIfOpen': boolean; + 'workbench.editor.decorations.badges': boolean; 'workbench.colorTheme': string; 'workbench.iconTheme': string; 'workbench.silentNotifications': boolean; diff --git a/packages/core/src/browser/decorations-service.ts b/packages/core/src/browser/decorations-service.ts index 0d175dd20b4fb..6c5586ba21870 100644 --- a/packages/core/src/browser/decorations-service.ts +++ b/packages/core/src/browser/decorations-service.ts @@ -78,7 +78,7 @@ class DecorationProviderWrapper { this.data.clear(); } else { for (const uri of uris) { - this.fetchData(new URI(uri.toString())); + this.fetchData(uri); const decoration = await provider.provideDecorations(uri, CancellationToken.None); if (decoration) { this.decorations.set(uri.toString(), decoration); @@ -131,14 +131,14 @@ class DecorationProviderWrapper { private fetchData(uri: URI): Decoration | undefined { // check for pending request and cancel it - const pendingRequest = this.data.get(new URI(uri.toString())); + const pendingRequest = this.data.get(uri); if (pendingRequest instanceof DecorationDataRequest) { pendingRequest.source.cancel(); this.data.delete(uri); } const source = new CancellationTokenSource(); - const dataOrThenable = this.provider.provideDecorations(new URI(uri.toString()), source.token); + const dataOrThenable = this.provider.provideDecorations(uri, source.token); if (!isThenable | undefined>(dataOrThenable)) { // sync -> we have a result now return this.keepItem(uri, dataOrThenable); @@ -197,7 +197,7 @@ export class DecorationsServiceImpl implements DecorationsService { const data: Decoration[] = []; let containsChildren: boolean = false; for (const wrapper of this.data) { - wrapper.getOrRetrieve(new URI(uri.toString()), includeChildren, (deco, isChild) => { + wrapper.getOrRetrieve(uri, includeChildren, (deco, isChild) => { if (!isChild || deco.bubble) { data.push(deco); containsChildren = isChild || containsChildren; diff --git a/packages/core/src/browser/dialogs.ts b/packages/core/src/browser/dialogs.ts index 2ab0a1203e976..c72fe33792ea6 100644 --- a/packages/core/src/browser/dialogs.ts +++ b/packages/core/src/browser/dialogs.ts @@ -458,40 +458,6 @@ export class ConfirmSaveDialog extends AbstractDialog { } -// Asks the user to confirm whether they want to exit with or without saving the changes -export async function confirmExitWithOrWithoutSaving(captionsToSave: string[], performSave: () => Promise): Promise { - const div: HTMLElement = document.createElement('div'); - div.innerText = nls.localizeByDefault("Your changes will be lost if you don't save them."); - - if (captionsToSave.length > 0) { - const span = document.createElement('span'); - span.appendChild(document.createElement('br')); - captionsToSave.forEach(cap => { - const b = document.createElement('b'); - b.innerText = cap; - span.appendChild(b); - span.appendChild(document.createElement('br')); - }); - span.appendChild(document.createElement('br')); - div.appendChild(span); - const result = await new ConfirmSaveDialog({ - title: nls.localizeByDefault('Do you want to save the changes to the following {0} files?', captionsToSave.length), - msg: div, - dontSave: nls.localizeByDefault("Don't Save"), - save: nls.localizeByDefault('Save All'), - cancel: Dialog.CANCEL - }).open(); - - if (result) { - await performSave(); - } - return result !== undefined; - } else { - // fallback if not passed with an empty caption-list. - return confirmExit(); - } - -} @injectable() export class SingleTextInputDialogProps extends DialogProps { readonly confirmButtonLabel?: string; diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 45e83b311720e..ea29eff0ebf87 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -35,7 +35,8 @@ import { MenuCommandAdapterRegistry, MenuCommandExecutor, MenuCommandAdapterRegistryImpl, - MenuCommandExecutorImpl + MenuCommandExecutorImpl, + MenuPath } from '../common'; import { KeybindingRegistry, KeybindingContext, KeybindingContribution } from './keybinding'; import { FrontendApplication } from './frontend-application'; @@ -126,7 +127,7 @@ import { DockPanel, RendererHost } from './widgets'; import { TooltipService, TooltipServiceImpl } from './tooltip-service'; import { BackendRequestService, RequestService, REQUEST_SERVICE_PATH } from '@theia/request'; import { bindFrontendStopwatch, bindBackendStopwatch } from './performance'; -import { SaveResourceService } from './save-resource-service'; +import { SaveableService } from './saveable-service'; import { SecondaryWindowHandler } from './secondary-window-handler'; import { UserWorkingDirectoryProvider } from './user-working-directory-provider'; import { WindowTitleService } from './window/window-title-service'; @@ -137,8 +138,13 @@ import { MarkdownRenderer, MarkdownRendererFactory, MarkdownRendererImpl } from import { StylingParticipant, StylingService } from './styling-service'; import { bindCommonStylingParticipants } from './common-styling-participants'; import { HoverService } from './hover-service'; -import { AdditionalViewsMenuWidget, AdditionalViewsMenuWidgetFactory } from './shell/additional-views-menu-widget'; +import { AdditionalViewsMenuPath, AdditionalViewsMenuWidget, AdditionalViewsMenuWidgetFactory } from './shell/additional-views-menu-widget'; import { LanguageIconLabelProvider } from './language-icon-provider'; +import { bindTreePreferences } from './tree'; +import { OpenWithService } from './open-with-service'; +import { ViewColumnService } from './shell/view-column-service'; +import { DomInputUndoRedoHandler, UndoRedoHandler, UndoRedoHandlerService } from './undo-redo-handler'; +import { WidgetStatusBarContribution, WidgetStatusBarService } from './widget-status-bar-service'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -174,9 +180,9 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(SidebarBottomMenuWidgetFactory).toAutoFactory(SidebarBottomMenuWidget); bind(AdditionalViewsMenuWidget).toSelf(); bind(AdditionalViewsMenuWidgetFactory).toFactory(ctx => (side: 'left' | 'right') => { - const widget = ctx.container.resolve(AdditionalViewsMenuWidget); - widget.side = side; - return widget; + const childContainer = ctx.container.createChild(); + childContainer.bind(AdditionalViewsMenuPath).toConstantValue(['additional_views_menu', side]); + return childContainer.resolve(AdditionalViewsMenuWidget); }); bind(SplitPositionHandler).toSelf().inSingletonScope(); @@ -199,7 +205,9 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is const commandService = container.get(CommandService); const corePreferences = container.get(CorePreferences); const hoverService = container.get(HoverService); - return new TabBarRenderer(contextMenuRenderer, tabBarDecoratorService, iconThemeService, selectionService, commandService, corePreferences, hoverService); + const contextKeyService: ContextKeyService = container.get(ContextKeyService); + return new TabBarRenderer(contextMenuRenderer, tabBarDecoratorService, iconThemeService, + selectionService, commandService, corePreferences, hoverService, contextKeyService); }); bind(TheiaDockPanel.Factory).toFactory(({ container }) => (options?: DockPanel.IOptions) => { const corePreferences = container.get(CorePreferences); @@ -221,6 +229,8 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(CommandOpenHandler).toSelf().inSingletonScope(); bind(OpenHandler).toService(CommandOpenHandler); + bind(OpenWithService).toSelf().inSingletonScope(); + bind(TooltipServiceImpl).toSelf().inSingletonScope(); bind(TooltipService).toService(TooltipServiceImpl); @@ -346,7 +356,6 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is }); bind(FrontendConnectionStatusService).toSelf().inSingletonScope(); bind(ConnectionStatusService).toService(FrontendConnectionStatusService); - bind(FrontendApplicationContribution).toService(FrontendConnectionStatusService); bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(ApplicationConnectionStatusContribution); @@ -366,6 +375,7 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(ThemeService).toSelf().inSingletonScope(); bindCorePreferences(bind); + bindTreePreferences(bind); bind(MimeService).toSelf().inSingletonScope(); @@ -443,8 +453,11 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bindFrontendStopwatch(bind); bindBackendStopwatch(bind); - bind(SaveResourceService).toSelf().inSingletonScope(); + bind(SaveableService).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(SaveableService); + bind(UserWorkingDirectoryProvider).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(UserWorkingDirectoryProvider); bind(HoverService).toSelf().inSingletonScope(); @@ -453,4 +466,14 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(FrontendApplicationContribution).toService(StylingService); bind(SecondaryWindowHandler).toSelf().inSingletonScope(); + bind(ViewColumnService).toSelf().inSingletonScope(); + + bind(UndoRedoHandlerService).toSelf().inSingletonScope(); + bindContributionProvider(bind, UndoRedoHandler); + bind(DomInputUndoRedoHandler).toSelf().inSingletonScope(); + bind(UndoRedoHandler).toService(DomInputUndoRedoHandler); + + bind(WidgetStatusBarService).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(WidgetStatusBarService); + bindContributionProvider(bind, WidgetStatusBarContribution); }); diff --git a/packages/core/src/browser/http-open-handler.ts b/packages/core/src/browser/http-open-handler.ts index e31b1f9bdc89d..d8f467a549372 100644 --- a/packages/core/src/browser/http-open-handler.ts +++ b/packages/core/src/browser/http-open-handler.ts @@ -27,6 +27,8 @@ export interface HttpOpenHandlerOptions { @injectable() export class HttpOpenHandler implements OpenHandler { + static readonly PRIORITY: number = 500; + readonly id = 'http'; @inject(WindowService) @@ -36,7 +38,7 @@ export class HttpOpenHandler implements OpenHandler { protected readonly externalUriService: ExternalUriService; canHandle(uri: URI, options?: HttpOpenHandlerOptions): number { - return ((options && options.openExternal) || uri.scheme.startsWith('http') || uri.scheme.startsWith('mailto')) ? 500 : 0; + return ((options && options.openExternal) || uri.scheme.startsWith('http') || uri.scheme.startsWith('mailto')) ? HttpOpenHandler.PRIORITY : 0; } async open(uri: URI): Promise { diff --git a/packages/core/src/browser/icon-registry.ts b/packages/core/src/browser/icon-registry.ts index 90c1088012f37..f41728d13046f 100644 --- a/packages/core/src/browser/icon-registry.ts +++ b/packages/core/src/browser/icon-registry.ts @@ -19,6 +19,7 @@ *--------------------------------------------------------------------------------------------*/ // code copied and modified from https://github.com/Microsoft/vscode/blob/main/src/vs/platform/theme/common/iconRegistry.ts +import { ThemeIcon } from '../common/theme'; import { URI } from 'vscode-uri'; export interface IconDefinition { @@ -48,16 +49,6 @@ export interface IconFontSource { readonly location: URI; readonly format: string; } - -export interface ThemeIcon { - readonly id: string; - readonly color?: ThemeColor; -} - -export interface ThemeColor { - id: string; -} - export const IconRegistry = Symbol('IconRegistry'); export interface IconRegistry { /** diff --git a/packages/core/src/browser/index.ts b/packages/core/src/browser/index.ts index eecaa565401dc..02cae0fbdf1f4 100644 --- a/packages/core/src/browser/index.ts +++ b/packages/core/src/browser/index.ts @@ -19,6 +19,7 @@ export * from './frontend-application'; export * from './frontend-application-contribution'; export * from './keyboard'; export * from './opener-service'; +export * from './open-with-service'; export * from './browser'; export * from './context-menu-renderer'; export * from './widgets'; @@ -45,3 +46,6 @@ export * from './tooltip-service'; export * from './decoration-style'; export * from './styling-service'; export * from './hover-service'; +export * from './saveable-service'; +export * from './undo-redo-handler'; +export * from './widget-status-bar-service'; diff --git a/packages/core/src/browser/json-schema-store.ts b/packages/core/src/browser/json-schema-store.ts index 729d14bfa1c45..e9401c3f191e9 100644 --- a/packages/core/src/browser/json-schema-store.ts +++ b/packages/core/src/browser/json-schema-store.ts @@ -18,9 +18,7 @@ import { injectable, inject, named } from 'inversify'; import { ContributionProvider } from '../common/contribution-provider'; import { FrontendApplicationContribution } from './frontend-application-contribution'; import { MaybePromise } from '../common'; -import { Endpoint } from './endpoint'; import { timeout, Deferred } from '../common/promise-util'; -import { RequestContext, RequestService } from '@theia/request'; export interface JsonSchemaConfiguration { fileMatch: string | string[]; @@ -95,16 +93,9 @@ export class JsonSchemaStore implements FrontendApplicationContribution { @injectable() export class DefaultJsonSchemaContribution implements JsonSchemaContribution { - - @inject(RequestService) - protected readonly requestService: RequestService; - - protected readonly jsonSchemaUrl = `${new Endpoint().httpScheme}//schemastore.org/api/json/catalog.json`; - async registerSchemas(context: JsonSchemaRegisterContext): Promise { - const response = await this.requestService.request({ url: this.jsonSchemaUrl }); - const schemas = RequestContext.asJson<{ schemas: DefaultJsonSchemaContribution.SchemaData[] }>(response).schemas; - for (const s of schemas) { + const catalog = require('./catalog.json') as { schemas: DefaultJsonSchemaContribution.SchemaData[] }; + for (const s of catalog.schemas) { if (s.fileMatch) { context.registerSchema({ fileMatch: s.fileMatch, diff --git a/packages/core/src/browser/keybinding.spec.ts b/packages/core/src/browser/keybinding.spec.ts index b556a505c849b..026b43b3f6f82 100644 --- a/packages/core/src/browser/keybinding.spec.ts +++ b/packages/core/src/browser/keybinding.spec.ts @@ -90,7 +90,8 @@ before(async () => { bind(StatusBar).toConstantValue({} as StatusBar); bind(MarkdownRendererImpl).toSelf().inSingletonScope(); bind(MarkdownRenderer).toService(MarkdownRendererImpl); - bind(MarkdownRendererFactory).toFactory(({ container }) => container.get(MarkdownRenderer)); + bind(MarkdownRendererFactory).toFactory(({ container }) => () => container.get(MarkdownRenderer)); + bind(CommandService).toService(CommandRegistry); bind(LabelParser).toSelf().inSingletonScope(); bind(ContextKeyService).to(ContextKeyServiceDummyImpl).inSingletonScope(); diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index a550ce746be3d..6871f5381ba62 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -497,6 +497,9 @@ export class KeybindingRegistry { isEnabledInScope(binding: common.Keybinding, target: HTMLElement | undefined): boolean { const context = binding.context && this.contexts[binding.context]; + if (binding.command && (!this.isPseudoCommand(binding.command) && !this.commandRegistry.isEnabled(binding.command, binding.args))) { + return false; + } if (context && !context.isEnabled(binding)) { return false; } diff --git a/packages/core/src/browser/markdown-rendering/markdown-renderer.ts b/packages/core/src/browser/markdown-rendering/markdown-renderer.ts index fa9eb5bcf031f..f3a9d93b611f4 100644 --- a/packages/core/src/browser/markdown-rendering/markdown-renderer.ts +++ b/packages/core/src/browser/markdown-rendering/markdown-renderer.ts @@ -24,7 +24,7 @@ import { codicon } from '../widgets'; // #region Copied from Copied from https://github.com/microsoft/vscode/blob/7d9b1c37f8e5ae3772782ba3b09d827eb3fdd833/src/vs/base/browser/formattedTextRenderer.ts export interface ContentActionHandler { - callback: (content: string, event?: MouseEvent) => void; + callback: (content: string, event?: MouseEvent | KeyboardEvent) => void; readonly disposables: DisposableGroup; } diff --git a/packages/core/src/browser/menu/browser-context-menu-renderer.ts b/packages/core/src/browser/menu/browser-context-menu-renderer.ts index 775efaec0e28f..1ae5f1f826878 100644 --- a/packages/core/src/browser/menu/browser-context-menu-renderer.ts +++ b/packages/core/src/browser/menu/browser-context-menu-renderer.ts @@ -40,7 +40,8 @@ export class BrowserContextMenuRenderer extends ContextMenuRenderer { if (onHide) { contextMenu.aboutToClose.connect(() => onHide!()); } - contextMenu.open(x, y); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + contextMenu.open(x, y, undefined, context); return new BrowserContextMenuAccess(contextMenu); } diff --git a/packages/core/src/browser/menu/browser-menu-plugin.ts b/packages/core/src/browser/menu/browser-menu-plugin.ts index 8145d9d58e84f..d1ceac06b8220 100644 --- a/packages/core/src/browser/menu/browser-menu-plugin.ts +++ b/packages/core/src/browser/menu/browser-menu-plugin.ts @@ -71,25 +71,30 @@ export class BrowserMainMenuFactory implements MenuWidgetFactory { const menuBar = new DynamicMenuBarWidget(); menuBar.id = 'theia:menubar'; this.corePreferences.ready.then(() => { - this.showMenuBar(menuBar, this.corePreferences.get('window.menuBarVisibility', 'classic')); - }); - const preferenceListener = this.corePreferences.onPreferenceChanged(preference => { - if (preference.preferenceName === 'window.menuBarVisibility') { - this.showMenuBar(menuBar, preference.newValue); - } - }); - const keybindingListener = this.keybindingRegistry.onKeybindingsChanged(() => { - const preference = this.corePreferences['window.menuBarVisibility']; - this.showMenuBar(menuBar, preference); - }); - menuBar.disposed.connect(() => { - preferenceListener.dispose(); - keybindingListener.dispose(); + this.showMenuBar(menuBar); }); + const disposable = new DisposableCollection( + this.corePreferences.onPreferenceChanged(change => { + if (change.preferenceName === 'window.menuBarVisibility') { + this.showMenuBar(menuBar, change.newValue); + } + }), + this.keybindingRegistry.onKeybindingsChanged(() => { + this.showMenuBar(menuBar); + }), + this.menuProvider.onDidChange(() => { + this.showMenuBar(menuBar); + }) + ); + menuBar.disposed.connect(() => disposable.dispose()); return menuBar; } - protected showMenuBar(menuBar: DynamicMenuBarWidget, preference: string | undefined): void { + protected getMenuBarVisibility(): string { + return this.corePreferences.get('window.menuBarVisibility', 'classic'); + } + + protected showMenuBar(menuBar: DynamicMenuBarWidget, preference = this.getMenuBarVisibility()): void { if (preference && ['classic', 'visible'].includes(preference)) { menuBar.clearMenus(); this.fillMenuBar(menuBar); @@ -187,13 +192,13 @@ export class DynamicMenuBarWidget extends MenuBarWidget { this.openActiveMenu(); await waitForRevealed(menu); - const menuPath = [label]; + const menuPath = [label, ...labels]; let current = menu; for (const itemLabel of labels) { const item = current.items.find(i => i.label === itemLabel); if (!item || !item.submenu) { - throw new Error(`could not find '${label}' submenu in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`); + throw new Error(`could not find '${itemLabel}' submenu in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`); } current.activeItem = item; current.triggerActiveItem(); @@ -211,7 +216,7 @@ export class DynamicMenuBarWidget extends MenuBarWidget { const menu = await this.activateMenu(menuPath[0], ...menuPath.slice(1)); const item = menu.items.find(i => i.label === labels[labels.length - 1]); if (!item) { - throw new Error(`could not find '${label}' item in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`); + throw new Error(`could not find '${labels[labels.length - 1]}' item in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`); } menu.activeItem = item; menu.triggerActiveItem(); @@ -267,14 +272,14 @@ export class DynamicMenuWidget extends MenuWidget { }); } - public override open(x: number, y: number, options?: MenuWidget.IOpenOptions): void { + public override open(x: number, y: number, options?: MenuWidget.IOpenOptions, anchor?: HTMLElement): void { const cb = () => { this.restoreFocusedElement(); this.aboutToClose.disconnect(cb); }; this.aboutToClose.connect(cb); this.preserveFocusedElement(); - super.open(x, y, options); + super.open(x, y, options, anchor); } protected updateSubMenus(parent: MenuWidget, menu: CompoundMenuNode, commands: MenuCommandRegistry): void { @@ -468,9 +473,11 @@ export class MenuCommandRegistry extends PhosphorCommandRegistry { }); const bindings = keybindingRegistry.getKeybindingsForCommand(id); - // Only consider the first keybinding. + // Only consider the first active keybinding. if (bindings.length) { - const binding = bindings[0]; + const binding = bindings.length > 1 ? + bindings.find(b => !b.when || this.services.contextKeyService.match(b.when)) ?? bindings[0] : + bindings[0]; const keys = keybindingRegistry.acceleratorFor(binding, ' ', true); this.addKeyBinding({ command: id, diff --git a/packages/core/src/browser/messaging/connection-source.ts b/packages/core/src/browser/messaging/connection-source.ts new file mode 100644 index 0000000000000..8e48110edb360 --- /dev/null +++ b/packages/core/src/browser/messaging/connection-source.ts @@ -0,0 +1,26 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { Channel, Event } from '../../common'; + +export const ConnectionSource = Symbol('ConnectionSource'); + +/** + * A ConnectionSource creates a Channel. The channel is valid until it sends a close event. + */ +export interface ConnectionSource { + onConnectionDidOpen: Event; +} diff --git a/packages/core/src/browser/messaging/frontend-id-provider.ts b/packages/core/src/browser/messaging/frontend-id-provider.ts new file mode 100644 index 0000000000000..9ec93c1b4200c --- /dev/null +++ b/packages/core/src/browser/messaging/frontend-id-provider.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { injectable } from 'inversify'; +import { generateUuid } from '../../common/uuid'; + +export const FrontendIdProvider = Symbol('FrontendIdProvider'); + +/** + * A FrontendIdProvider computes an id for an instance of the front end that may be reconnected to a back end + * connection context. + */ +export interface FrontendIdProvider { + getId(): string; +} + +@injectable() +export class BrowserFrontendIdProvider implements FrontendIdProvider { + protected readonly id = generateUuid(); // generate a new id each time we load the application + + getId(): string { + return this.id; + } +} diff --git a/packages/core/src/browser/messaging/index.ts b/packages/core/src/browser/messaging/index.ts index c2b82e9ab402d..5112fc973508d 100644 --- a/packages/core/src/browser/messaging/index.ts +++ b/packages/core/src/browser/messaging/index.ts @@ -15,3 +15,4 @@ // ***************************************************************************** export * from './ws-connection-provider'; +export * from './service-connection-provider'; diff --git a/packages/core/src/browser/messaging/messaging-frontend-module.ts b/packages/core/src/browser/messaging/messaging-frontend-module.ts index f852c8f651231..a7c742c0cb9d8 100644 --- a/packages/core/src/browser/messaging/messaging-frontend-module.ts +++ b/packages/core/src/browser/messaging/messaging-frontend-module.ts @@ -15,9 +15,27 @@ // ***************************************************************************** import { ContainerModule } from 'inversify'; -import { LocalWebSocketConnectionProvider, WebSocketConnectionProvider } from './ws-connection-provider'; +import { BrowserFrontendIdProvider, FrontendIdProvider } from './frontend-id-provider'; +import { WebSocketConnectionSource } from './ws-connection-source'; +import { LocalConnectionProvider, RemoteConnectionProvider, ServiceConnectionProvider } from './service-connection-provider'; +import { ConnectionSource } from './connection-source'; +import { ConnectionCloseService, connectionCloseServicePath } from '../../common/messaging/connection-management'; +import { WebSocketConnectionProvider } from './ws-connection-provider'; + +const backendServiceProvider = Symbol('backendServiceProvider'); export const messagingFrontendModule = new ContainerModule(bind => { + bind(ConnectionCloseService).toDynamicValue(ctx => WebSocketConnectionProvider.createProxy(ctx.container, connectionCloseServicePath)).inSingletonScope(); + bind(BrowserFrontendIdProvider).toSelf().inSingletonScope(); + bind(FrontendIdProvider).toService(BrowserFrontendIdProvider); + bind(WebSocketConnectionSource).toSelf().inSingletonScope(); + bind(backendServiceProvider).toDynamicValue(ctx => { + bind(ServiceConnectionProvider).toSelf().inSingletonScope(); + const container = ctx.container.createChild(); + container.bind(ConnectionSource).toService(WebSocketConnectionSource); + return container.get(ServiceConnectionProvider); + }).inSingletonScope(); + bind(LocalConnectionProvider).toService(backendServiceProvider); + bind(RemoteConnectionProvider).toService(backendServiceProvider); bind(WebSocketConnectionProvider).toSelf().inSingletonScope(); - bind(LocalWebSocketConnectionProvider).toService(WebSocketConnectionProvider); }); diff --git a/packages/core/src/browser/messaging/service-connection-provider.ts b/packages/core/src/browser/messaging/service-connection-provider.ts new file mode 100644 index 0000000000000..aee98b88e6f4c --- /dev/null +++ b/packages/core/src/browser/messaging/service-connection-provider.ts @@ -0,0 +1,140 @@ +// ***************************************************************************** +// Copyright (C) 2020 Ericsson 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, interfaces, postConstruct } from 'inversify'; +import { Channel, RpcProxy, RpcProxyFactory } from '../../common'; +import { ChannelMultiplexer } from '../../common/message-rpc/channel'; +import { Deferred } from '../../common/promise-util'; +import { ConnectionSource } from './connection-source'; + +/** + * Service id for the local connection provider + */ +export const LocalConnectionProvider = Symbol('LocalConnectionProvider'); +/** + * Service id for the remote connection provider + */ +export const RemoteConnectionProvider = Symbol('RemoteConnectionProvider'); + +export namespace ServiceConnectionProvider { + export type ConnectionHandler = (path: String, channel: Channel) => void; +} + +/** + * This class manages the channels for remote services in the back end. + * + * Since we have the ability to use a remote back end via SSH, we need to distinguish + * between two types of services: those that will be redirected to the remote back end + * and those which must remain in the local back end. For example the service that manages + * the remote ssh connections and port forwarding to the remote instance must remain local + * while e.g. the file system service will run in the remote back end. For each set + * of services, we will bind an instance of this class to {@linkcode LocalConnectionProvider} + * and {@linkcode RemoteConnectionProvider} respectively. + */ +@injectable() +export class ServiceConnectionProvider { + + static createProxy(container: interfaces.Container, path: string, arg?: object): RpcProxy { + return container.get(RemoteConnectionProvider).createProxy(path, arg); + } + + static createLocalProxy(container: interfaces.Container, path: string, arg?: object): RpcProxy { + return container.get(LocalConnectionProvider).createProxy(path, arg); + } + + static createHandler(container: interfaces.Container, path: string, arg?: object): void { + const remote = container.get(RemoteConnectionProvider); + const local = container.get(LocalConnectionProvider); + remote.createProxy(path, arg); + if (remote !== local) { + local.createProxy(path, arg); + } + } + + protected readonly channelHandlers = new Map(); + + /** + * Create a proxy object to remote interface of T type + * over a web socket connection for the given path and proxy factory. + */ + createProxy(path: string, factory: RpcProxyFactory): RpcProxy; + /** + * Create a proxy object to remote interface of T type + * over a web socket connection for the given path. + * + * An optional target can be provided to handle + * notifications and requests from a remote side. + */ + createProxy(path: string, target?: object): RpcProxy; + createProxy(path: string, arg?: object): RpcProxy { + const factory = arg instanceof RpcProxyFactory ? arg : new RpcProxyFactory(arg); + this.listen(path, (_, c) => factory.listen(c), true); + return factory.createProxy(); + } + + protected channelMultiplexer: ChannelMultiplexer; + + private channelReadyDeferred = new Deferred(); + protected get channelReady(): Promise { + return this.channelReadyDeferred.promise; + } + + @postConstruct() + init(): void { + this.connectionSource.onConnectionDidOpen(channel => this.handleChannelCreated(channel)); + } + + @inject(ConnectionSource) + protected connectionSource: ConnectionSource; + + /** + * This method must be invoked by subclasses when they have created the main channel. + * @param mainChannel + */ + protected handleChannelCreated(channel: Channel): void { + channel.onClose(() => { + this.handleChannelClosed(channel); + }); + + this.channelMultiplexer = new ChannelMultiplexer(channel); + this.channelReadyDeferred.resolve(); + for (const entry of this.channelHandlers.entries()) { + this.openChannel(entry[0], entry[1]); + } + } + + handleChannelClosed(channel: Channel): void { + this.channelReadyDeferred = new Deferred(); + } + + /** + * Install a connection handler for the given path. + */ + listen(path: string, handler: ServiceConnectionProvider.ConnectionHandler, reconnect: boolean): void { + this.openChannel(path, handler).then(() => { + if (reconnect) { + this.channelHandlers.set(path, handler); + } + }); + + } + + private async openChannel(path: string, handler: ServiceConnectionProvider.ConnectionHandler): Promise { + await this.channelReady; + const newChannel = await this.channelMultiplexer.open(path); + handler(path, newChannel); + } +} diff --git a/packages/core/src/browser/messaging/ws-connection-provider.ts b/packages/core/src/browser/messaging/ws-connection-provider.ts index 822110c58fd8b..642b3c3d26972 100644 --- a/packages/core/src/browser/messaging/ws-connection-provider.ts +++ b/packages/core/src/browser/messaging/ws-connection-provider.ts @@ -14,160 +14,36 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, interfaces, decorate, unmanaged } from 'inversify'; -import { RpcProxyFactory, RpcProxy, Emitter, Event, Channel } from '../../common'; -import { Endpoint } from '../endpoint'; -import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; -import { io, Socket } from 'socket.io-client'; -import { IWebSocket, WebSocketChannel } from '../../common/messaging/web-socket-channel'; +import { injectable, interfaces, decorate, unmanaged, inject } from 'inversify'; +import { RpcProxyFactory, RpcProxy } from '../../common'; +import { RemoteConnectionProvider, ServiceConnectionProvider } from './service-connection-provider'; decorate(injectable(), RpcProxyFactory); decorate(unmanaged(), RpcProxyFactory, 0); -export const LocalWebSocketConnectionProvider = Symbol('LocalWebSocketConnectionProvider'); - -export interface WebSocketOptions { - /** - * True by default. - */ - reconnecting?: boolean; -} - +/** + * @deprecated This class serves to keep API compatibility for a while. + * Use the {@linkcode RemoteConnectionProvider} as the injection symbol and {@linkcode ServiceConnectionProvider} as the type instead. + */ @injectable() -export class WebSocketConnectionProvider extends AbstractConnectionProvider { - - protected readonly onSocketDidOpenEmitter: Emitter = new Emitter(); - get onSocketDidOpen(): Event { - return this.onSocketDidOpenEmitter.event; - } - - protected readonly onSocketDidCloseEmitter: Emitter = new Emitter(); - get onSocketDidClose(): Event { - return this.onSocketDidCloseEmitter.event; - } +export class WebSocketConnectionProvider { + @inject(RemoteConnectionProvider) + private readonly remoteConnectionProvider: ServiceConnectionProvider; - static override createProxy(container: interfaces.Container, path: string, arg?: object): RpcProxy { - return container.get(WebSocketConnectionProvider).createProxy(path, arg); + static createProxy(container: interfaces.Container, path: string, arg?: object): RpcProxy { + return ServiceConnectionProvider.createProxy(container, path, arg); } static createLocalProxy(container: interfaces.Container, path: string, arg?: object): RpcProxy { - return container.get(LocalWebSocketConnectionProvider).createProxy(path, arg); + return ServiceConnectionProvider.createLocalProxy(container, path, arg); } static createHandler(container: interfaces.Container, path: string, arg?: object): void { - const remote = container.get(WebSocketConnectionProvider); - const local = container.get(LocalWebSocketConnectionProvider); - remote.createProxy(path, arg); - if (remote !== local) { - local.createProxy(path, arg); - } + return ServiceConnectionProvider.createHandler(container, path, arg); } - protected readonly socket: Socket; - - constructor() { - super(); - const url = this.createWebSocketUrl(WebSocketChannel.wsPath); - this.socket = this.createWebSocket(url); - this.socket.on('connect', () => { - this.initializeMultiplexer(); - if (this.reconnectChannelOpeners.length > 0) { - this.reconnectChannelOpeners.forEach(opener => opener()); - this.reconnectChannelOpeners = []; - } - this.socket.on('disconnect', () => this.fireSocketDidClose()); - this.socket.on('message', () => this.onIncomingMessageActivityEmitter.fire(undefined)); - this.fireSocketDidOpen(); - }); - this.socket.connect(); - } - - protected createMainChannel(): Channel { - return new WebSocketChannel(this.toIWebSocket(this.socket)); - } - - protected toIWebSocket(socket: Socket): IWebSocket { - return { - close: () => { - socket.removeAllListeners('disconnect'); - socket.removeAllListeners('error'); - socket.removeAllListeners('message'); - }, - isConnected: () => socket.connected, - onClose: cb => socket.on('disconnect', reason => cb(reason)), - onError: cb => socket.on('error', reason => cb(reason)), - onMessage: cb => socket.on('message', data => cb(data)), - send: message => socket.emit('message', message) - }; - } - - override async openChannel(path: string, handler: (channel: Channel) => void, options?: WebSocketOptions): Promise { - if (this.socket.connected) { - return super.openChannel(path, handler, options); - } else { - const openChannel = () => { - this.socket.off('connect', openChannel); - this.openChannel(path, handler, options); - }; - this.socket.on('connect', openChannel); - } - } - - /** - * @param path The handler to reach in the backend. - */ - protected createWebSocketUrl(path: string): string { - // Since we are using Socket.io, the path should look like the following: - // proto://domain.com/{path} - return this.createEndpoint(path).getWebSocketUrl().withPath(path).toString(); - } - - protected createHttpWebSocketUrl(path: string): string { - return this.createEndpoint(path).getRestUrl().toString(); - } - - protected createEndpoint(path: string): Endpoint { - return new Endpoint({ path }); - } - - /** - * Creates a web socket for the given url - */ - protected createWebSocket(url: string): Socket { - return io(url, { - path: this.createSocketIoPath(url), - reconnection: true, - reconnectionDelay: 1000, - reconnectionDelayMax: 10000, - reconnectionAttempts: Infinity, - extraHeaders: { - // Socket.io strips the `origin` header - // We need to provide our own for validation - 'fix-origin': window.location.origin - } - }); - } - - /** - * Path for Socket.io to make its requests to. - */ - protected createSocketIoPath(url: string): string | undefined { - if (location.protocol === Endpoint.PROTO_FILE) { - return '/socket.io'; - } - let { pathname } = location; - if (!pathname.endsWith('/')) { - pathname += '/'; - } - return pathname + 'socket.io'; - } - - protected fireSocketDidOpen(): void { - this.onSocketDidOpenEmitter.fire(undefined); - } - - protected fireSocketDidClose(): void { - this.onSocketDidCloseEmitter.fire(undefined); + createProxy(path: string, target?: object): RpcProxy; + createProxy(path: string, factory: RpcProxyFactory): RpcProxy { + return this.remoteConnectionProvider.createProxy(path, factory); } } - diff --git a/packages/core/src/browser/messaging/ws-connection-source.ts b/packages/core/src/browser/messaging/ws-connection-source.ts new file mode 100644 index 0000000000000..24d7d7920143f --- /dev/null +++ b/packages/core/src/browser/messaging/ws-connection-source.ts @@ -0,0 +1,230 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { AbstractChannel, Channel, Disposable, DisposableCollection, Emitter, Event, servicesPath } from '../../common'; +import { ConnectionSource } from './connection-source'; +import { Socket, io } from 'socket.io-client'; +import { Endpoint } from '../endpoint'; +import { ForwardingChannel } from '../../common/message-rpc/channel'; +import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer'; +import { inject, injectable, postConstruct } from 'inversify'; +import { FrontendIdProvider } from './frontend-id-provider'; +import { FrontendApplicationConfigProvider } from '../frontend-application-config-provider'; +import { SocketWriteBuffer } from '../../common/messaging/socket-write-buffer'; +import { ConnectionManagementMessages } from '../../common/messaging/connection-management'; + +@injectable() +export class WebSocketConnectionSource implements ConnectionSource { + static readonly NO_CONNECTION = ''; + + @inject(FrontendIdProvider) + protected readonly frontendIdProvider: FrontendIdProvider; + + private readonly writeBuffer = new SocketWriteBuffer(); + + private _socket: Socket; + get socket(): Socket { + return this._socket; + } + + protected currentChannel: AbstractChannel; + + protected readonly onConnectionDidOpenEmitter: Emitter = new Emitter(); + get onConnectionDidOpen(): Event { + return this.onConnectionDidOpenEmitter.event; + } + + protected readonly onSocketDidOpenEmitter: Emitter = new Emitter(); + get onSocketDidOpen(): Event { + return this.onSocketDidOpenEmitter.event; + } + + protected readonly onSocketDidCloseEmitter: Emitter = new Emitter(); + get onSocketDidClose(): Event { + return this.onSocketDidCloseEmitter.event; + } + + protected readonly onIncomingMessageActivityEmitter: Emitter = new Emitter(); + get onIncomingMessageActivity(): Event { + return this.onIncomingMessageActivityEmitter.event; + } + + constructor() { + } + + @postConstruct() + openSocket(): void { + const url = this.createWebSocketUrl(servicesPath); + this._socket = this.createWebSocket(url); + + this._socket.on('connect', () => { + this.onSocketDidOpenEmitter.fire(); + this.handleSocketConnected(); + }); + + this._socket.on('disconnect', () => { + this.onSocketDidCloseEmitter.fire(); + }); + + this._socket.on('error', reason => { + if (this.currentChannel) { + this.currentChannel.onErrorEmitter.fire(reason); + }; + }); + this._socket.connect(); + } + + protected negogiateReconnect(): void { + const reconnectListener = (hasConnection: boolean) => { + this._socket.off(ConnectionManagementMessages.RECONNECT, reconnectListener); + if (hasConnection) { + console.info(`reconnect succeeded on ${this.socket.id}`); + this.writeBuffer!.flush(this.socket); + } else { + if (FrontendApplicationConfigProvider.get().reloadOnReconnect) { + window.location.reload(); // this might happen in the preload module, when we have no window service yet + } else { + console.info(`reconnect failed on ${this.socket.id}`); + this.currentChannel.onCloseEmitter.fire({ reason: 'reconnecting channel' }); + this.currentChannel.close(); + this.writeBuffer.drain(); + this.socket.disconnect(); + this.socket.connect(); + this.negotiateInitialConnect(); + } + } + }; + this._socket.on(ConnectionManagementMessages.RECONNECT, reconnectListener); + console.info(`sending reconnect on ${this.socket.id}`); + this._socket.emit(ConnectionManagementMessages.RECONNECT, this.frontendIdProvider.getId()); + } + + protected negotiateInitialConnect(): void { + const initialConnectListener = () => { + console.info(`initial connect received on ${this.socket.id}`); + + this._socket.off(ConnectionManagementMessages.INITIAL_CONNECT, initialConnectListener); + this.connectNewChannel(); + }; + this._socket.on(ConnectionManagementMessages.INITIAL_CONNECT, initialConnectListener); + console.info(`sending initial connect on ${this.socket.id}`); + + this._socket.emit(ConnectionManagementMessages.INITIAL_CONNECT, this.frontendIdProvider.getId()); + } + + protected handleSocketConnected(): void { + if (this.currentChannel) { + this.negogiateReconnect(); + } else { + this.negotiateInitialConnect(); + } + } + + connectNewChannel(): void { + if (this.currentChannel) { + this.currentChannel.close(); + this.currentChannel.onCloseEmitter.fire({ reason: 'reconnecting channel' }); + } + this.writeBuffer.drain(); + this.currentChannel = this.createChannel(); + this.onConnectionDidOpenEmitter.fire(this.currentChannel); + } + + protected createChannel(): AbstractChannel { + const toDispose = new DisposableCollection(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const messageHandler = (data: any) => { + this.onIncomingMessageActivityEmitter.fire(); + if (this.currentChannel) { + // In the browser context socketIO receives binary messages as ArrayBuffers. + // So we have to convert them to a Uint8Array before delegating the message to the read buffer. + const buffer = data instanceof ArrayBuffer ? new Uint8Array(data) : data; + this.currentChannel.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(buffer)); + }; + }; + this._socket.on('message', messageHandler); + toDispose.push(Disposable.create(() => { + this.socket.off('message', messageHandler); + })); + + const channel = new ForwardingChannel('any', () => { + toDispose.dispose(); + }, () => { + const result = new Uint8ArrayWriteBuffer(); + + result.onCommit(buffer => { + if (this.socket.connected) { + this.socket.send(buffer); + } else { + this.writeBuffer.buffer(buffer); + } + }); + + return result; + }); + return channel; + } + + /** + * @param path The handler to reach in the backend. + */ + protected createWebSocketUrl(path: string): string { + // Since we are using Socket.io, the path should look like the following: + // proto://domain.com/{path} + return this.createEndpoint(path).getWebSocketUrl().withPath(path).toString(); + } + + protected createHttpWebSocketUrl(path: string): string { + return this.createEndpoint(path).getRestUrl().toString(); + } + + protected createEndpoint(path: string): Endpoint { + return new Endpoint({ path }); + } + + /** + * Creates a web socket for the given url + */ + protected createWebSocket(url: string): Socket { + return io(url, { + path: this.createSocketIoPath(url), + reconnection: true, + reconnectionDelay: 1000, + reconnectionDelayMax: 10000, + reconnectionAttempts: Infinity, + extraHeaders: { + // Socket.io strips the `origin` header + // We need to provide our own for validation + 'fix-origin': window.location.origin + } + }); + } + + /** + * Path for Socket.io to make its requests to. + */ + protected createSocketIoPath(url: string): string | undefined { + if (location.protocol === Endpoint.PROTO_FILE) { + return '/socket.io'; + } + let { pathname } = location; + if (!pathname.endsWith('/')) { + pathname += '/'; + } + return pathname + 'socket.io'; + } +} diff --git a/packages/core/src/browser/open-with-service.ts b/packages/core/src/browser/open-with-service.ts new file mode 100644 index 0000000000000..56eb7c4d54a93 --- /dev/null +++ b/packages/core/src/browser/open-with-service.ts @@ -0,0 +1,144 @@ +// ***************************************************************************** +// 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 { inject, injectable } from 'inversify'; +import { Disposable } from '../common/disposable'; +import { nls } from '../common/nls'; +import { MaybePromise } from '../common/types'; +import { URI } from '../common/uri'; +import { QuickInputService, QuickPickItem, QuickPickItemOrSeparator } from './quick-input'; +import { PreferenceScope, PreferenceService } from './preferences'; +import { getDefaultHandler } from './opener-service'; + +export interface OpenWithHandler { + /** + * A unique id of this handler. + */ + readonly id: string; + /** + * A human-readable name of this handler. + */ + readonly label?: string; + /** + * A human-readable provider name of this handler. + */ + readonly providerName?: string; + /** + * A css icon class of this handler. + */ + readonly iconClass?: string; + /** + * Test whether this handler can open the given URI for given options. + * Return a nonzero number if this handler can open; otherwise it cannot. + * Never reject. + * + * A returned value indicating a priority of this handler. + */ + canHandle(uri: URI): number; + /** + * Test whether this handler and open the given URI + * and return the order of this handler in the list. + */ + getOrder?(uri: URI): number; + /** + * Open a widget for the given URI and options. + * Resolve to an opened widget or undefined, e.g. if a page is opened. + * Never reject if `canHandle` return a positive number; otherwise should reject. + */ + open(uri: URI): MaybePromise; +} + +export interface OpenWithQuickPickItem extends QuickPickItem { + handler: OpenWithHandler; +} + +@injectable() +export class OpenWithService { + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + protected readonly handlers: OpenWithHandler[] = []; + + registerHandler(handler: OpenWithHandler): Disposable { + if (this.handlers.some(h => h.id === handler.id)) { + console.warn('Duplicate OpenWithHandler registration: ' + handler.id); + return Disposable.NULL; + } + this.handlers.push(handler); + return Disposable.create(() => { + const index = this.handlers.indexOf(handler); + if (index !== -1) { + this.handlers.splice(index, 1); + } + }); + } + + async openWith(uri: URI): Promise { + // Clone the object, because all objects returned by the preferences service are frozen. + const associations: Record = { ...this.preferenceService.get('workbench.editorAssociations') }; + const ext = `*${uri.path.ext}`; + const handlers = this.getHandlers(uri); + const ordered = handlers.slice().sort((a, b) => this.getOrder(b, uri) - this.getOrder(a, uri)); + const defaultHandler = getDefaultHandler(uri, this.preferenceService) ?? handlers[0]?.id; + const items = this.getQuickPickItems(ordered, defaultHandler); + // Only offer to select a default editor when the file has a file extension + const extraItems: QuickPickItemOrSeparator[] = uri.path.ext ? [{ + type: 'separator' + }, { + label: nls.localizeByDefault("Configure default editor for '{0}'...", ext) + }] : []; + const result = await this.quickInputService.pick([...items, ...extraItems], { + placeHolder: nls.localizeByDefault("Select editor for '{0}'", uri.path.base) + }); + if (result) { + if ('handler' in result) { + return result.handler.open(uri); + } else if (result.label) { + const configureResult = await this.quickInputService.pick(items, { + placeHolder: nls.localizeByDefault("Select new default editor for '{0}'", ext) + }); + if (configureResult) { + associations[ext] = configureResult.handler.id; + this.preferenceService.set('workbench.editorAssociations', associations, PreferenceScope.User); + return configureResult.handler.open(uri); + } + } + } + return undefined; + } + + protected getQuickPickItems(handlers: OpenWithHandler[], defaultHandler?: string): OpenWithQuickPickItem[] { + return handlers.map(handler => ({ + handler, + label: handler.label ?? handler.id, + detail: handler.providerName ?? '', + description: handler.id === defaultHandler ? nls.localizeByDefault('Default') : undefined + })); + } + + protected getOrder(handler: OpenWithHandler, uri: URI): number { + return handler.getOrder ? handler.getOrder(uri) : handler.canHandle(uri); + } + + getHandlers(uri: URI): OpenWithHandler[] { + const map = new Map(this.handlers.map(handler => [handler, handler.canHandle(uri)])); + return this.handlers.filter(handler => map.get(handler)! > 0).sort((a, b) => map.get(b)! - map.get(a)!); + } +} diff --git a/packages/core/src/browser/opener-service.ts b/packages/core/src/browser/opener-service.ts index e15418e3e6b29..f8362fa4cc747 100644 --- a/packages/core/src/browser/opener-service.ts +++ b/packages/core/src/browser/opener-service.ts @@ -17,6 +17,8 @@ import { named, injectable, inject } from 'inversify'; import URI from '../common/uri'; import { ContributionProvider, Prioritizeable, MaybePromise, Emitter, Event, Disposable } from '../common'; +import { PreferenceService } from './preferences'; +import { match } from '../common/glob'; export interface OpenerOptions { } @@ -79,6 +81,12 @@ export interface OpenerService { * Add open handler i.e. for custom editors */ addHandler?(openHandler: OpenHandler): Disposable; + + /** + * Remove open handler + */ + removeHandler?(openHandler: OpenHandler): void; + /** * Event that fires when a new opener is added or removed. */ @@ -90,6 +98,17 @@ export async function open(openerService: OpenerService, uri: URI, options?: Ope return opener.open(uri, options); } +export function getDefaultHandler(uri: URI, preferenceService: PreferenceService): string | undefined { + const associations = preferenceService.get('workbench.editorAssociations', {}); + const defaultHandler = Object.entries(associations).find(([key]) => match(key, uri.path.base))?.[1]; + if (typeof defaultHandler === 'string') { + return defaultHandler; + } + return undefined; +} + +export const defaultHandlerPriority = 100_000; + @injectable() export class DefaultOpenerService implements OpenerService { // Collection of open-handlers for custom-editor contributions. @@ -108,11 +127,15 @@ export class DefaultOpenerService implements OpenerService { this.onDidChangeOpenersEmitter.fire(); return Disposable.create(() => { - this.customEditorOpenHandlers.splice(this.customEditorOpenHandlers.indexOf(openHandler), 1); - this.onDidChangeOpenersEmitter.fire(); + this.removeHandler(openHandler); }); } + removeHandler(openHandler: OpenHandler): void { + this.customEditorOpenHandlers.splice(this.customEditorOpenHandlers.indexOf(openHandler), 1); + this.onDidChangeOpenersEmitter.fire(); + } + async getOpener(uri: URI, options?: OpenerOptions): Promise { const handlers = await this.prioritize(uri, options); if (handlers.length >= 1) { diff --git a/packages/core/src/browser/preload/i18n-preload-contribution.ts b/packages/core/src/browser/preload/i18n-preload-contribution.ts index 309c8df5f9436..0cddfa112e723 100644 --- a/packages/core/src/browser/preload/i18n-preload-contribution.ts +++ b/packages/core/src/browser/preload/i18n-preload-contribution.ts @@ -33,7 +33,7 @@ export class I18nPreloadContribution implements PreloadContribution { locale: defaultLocale }); } - if (nls.locale) { + if (nls.locale && nls.locale !== nls.defaultLocale) { const localization = await this.localizationServer.loadLocalization(nls.locale); if (localization.languagePack) { nls.localization = localization; diff --git a/packages/core/src/browser/save-resource-service.ts b/packages/core/src/browser/save-resource-service.ts deleted file mode 100644 index 6eb5bd876e43d..0000000000000 --- a/packages/core/src/browser/save-resource-service.ts +++ /dev/null @@ -1,59 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2022 Arm 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 'inversify'; -import { MessageService, UNTITLED_SCHEME } from '../common'; -import { Navigatable, NavigatableWidget } from './navigatable-types'; -import { Saveable, SaveableSource, SaveOptions } from './saveable'; -import { Widget } from './widgets'; - -@injectable() -export class SaveResourceService { - @inject(MessageService) protected readonly messageService: MessageService; - - /** - * Indicate if the document can be saved ('Save' command should be disable if not). - */ - canSave(widget?: Widget): widget is Widget & (Saveable | SaveableSource) { - return Saveable.isDirty(widget) && (this.canSaveNotSaveAs(widget) || this.canSaveAs(widget)); - } - - canSaveNotSaveAs(widget?: Widget): widget is Widget & (Saveable | SaveableSource) { - // By default, we never allow a document to be saved if it is untitled. - return Boolean(widget && NavigatableWidget.getUri(widget)?.scheme !== UNTITLED_SCHEME); - } - - /** - * Saves the document - * - * No op if the widget is not saveable. - */ - async save(widget: Widget | undefined, options?: SaveOptions): Promise { - if (this.canSaveNotSaveAs(widget)) { - await Saveable.save(widget, options); - } else if (this.canSaveAs(widget)) { - await this.saveAs(widget, options); - } - } - - canSaveAs(saveable?: Widget): saveable is Widget & SaveableSource & Navigatable { - return false; - } - - saveAs(sourceWidget: Widget & SaveableSource & Navigatable, options?: SaveOptions): Promise { - return Promise.reject('Unsupported: The base SaveResourceService does not support saveAs action.'); - } -} diff --git a/packages/core/src/browser/saveable-service.ts b/packages/core/src/browser/saveable-service.ts new file mode 100644 index 0000000000000..15fbf85fe3c1d --- /dev/null +++ b/packages/core/src/browser/saveable-service.ts @@ -0,0 +1,332 @@ +/******************************************************************************** + * Copyright (C) 2022 Arm 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 type { ApplicationShell } from './shell'; +import { injectable } from 'inversify'; +import { UNTITLED_SCHEME, URI, Disposable, DisposableCollection, Emitter, Event } from '../common'; +import { Navigatable, NavigatableWidget } from './navigatable-types'; +import { AutoSaveMode, Saveable, SaveableSource, SaveableWidget, SaveOptions, SaveReason, setDirty, close, PostCreationSaveableWidget, ShouldSaveDialog } from './saveable'; +import { waitForClosed, Widget } from './widgets'; +import { FrontendApplicationContribution } from './frontend-application-contribution'; +import { FrontendApplication } from './frontend-application'; +import throttle = require('lodash.throttle'); + +@injectable() +export class SaveableService implements FrontendApplicationContribution { + + protected saveThrottles = new Map(); + protected saveMode: AutoSaveMode = 'off'; + protected saveDelay = 1000; + protected shell: ApplicationShell; + + protected readonly onDidAutoSaveChangeEmitter = new Emitter(); + protected readonly onDidAutoSaveDelayChangeEmitter = new Emitter(); + + get onDidAutoSaveChange(): Event { + return this.onDidAutoSaveChangeEmitter.event; + } + + get onDidAutoSaveDelayChange(): Event { + return this.onDidAutoSaveDelayChangeEmitter.event; + } + + get autoSave(): AutoSaveMode { + return this.saveMode; + } + + set autoSave(value: AutoSaveMode) { + this.updateAutoSaveMode(value); + } + + get autoSaveDelay(): number { + return this.saveDelay; + } + + set autoSaveDelay(value: number) { + this.updateAutoSaveDelay(value); + } + + onDidInitializeLayout(app: FrontendApplication): void { + this.shell = app.shell; + // Register restored editors first + for (const widget of this.shell.widgets) { + const saveable = Saveable.get(widget); + if (saveable) { + this.registerSaveable(widget, saveable); + } + } + this.shell.onDidAddWidget(e => { + const saveable = Saveable.get(e); + if (saveable) { + this.registerSaveable(e, saveable); + } + }); + this.shell.onDidChangeCurrentWidget(e => { + if (this.saveMode === 'onFocusChange') { + const widget = e.oldValue; + const saveable = Saveable.get(widget); + if (saveable && widget && this.shouldAutoSave(widget, saveable)) { + saveable.save({ + saveReason: SaveReason.FocusChange + }); + } + } + }); + this.shell.onDidRemoveWidget(e => { + this.saveThrottles.get(e)?.dispose(); + this.saveThrottles.delete(e); + }); + } + + protected updateAutoSaveMode(mode: AutoSaveMode): void { + this.saveMode = mode; + this.onDidAutoSaveChangeEmitter.fire(mode); + if (mode === 'onFocusChange') { + // If the new mode is onFocusChange, we need to save all dirty documents that are not focused + const widgets = this.shell.widgets; + for (const widget of widgets) { + const saveable = Saveable.get(widget); + if (saveable && widget !== this.shell.currentWidget && this.shouldAutoSave(widget, saveable)) { + saveable.save({ + saveReason: SaveReason.FocusChange + }); + } + } + } + } + + protected updateAutoSaveDelay(delay: number): void { + this.saveDelay = delay; + this.onDidAutoSaveDelayChangeEmitter.fire(delay); + } + + registerSaveable(widget: Widget, saveable: Saveable): Disposable { + const saveThrottle = new AutoSaveThrottle( + saveable, + this, + () => { + if (this.saveMode === 'afterDelay' && this.shouldAutoSave(widget, saveable)) { + saveable.save({ + saveReason: SaveReason.AfterDelay + }); + } + }, + this.addBlurListener(widget, saveable) + ); + this.saveThrottles.set(widget, saveThrottle); + this.applySaveableWidget(widget, saveable); + return saveThrottle; + } + + protected addBlurListener(widget: Widget, saveable: Saveable): Disposable { + const document = widget.node.ownerDocument; + const listener = (() => { + if (this.saveMode === 'onWindowChange' && !this.windowHasFocus(document) && this.shouldAutoSave(widget, saveable)) { + saveable.save({ + saveReason: SaveReason.FocusChange + }); + } + }).bind(this); + document.addEventListener('blur', listener); + return Disposable.create(() => { + document.removeEventListener('blur', listener); + }); + } + + protected windowHasFocus(document: Document): boolean { + if (document.visibilityState === 'hidden') { + return false; + } else if (document.hasFocus()) { + return true; + } + // TODO: Add support for iframes + return false; + } + + protected shouldAutoSave(widget: Widget, saveable: Saveable): boolean { + const uri = NavigatableWidget.getUri(widget); + if (uri?.scheme === UNTITLED_SCHEME) { + // Never auto-save untitled documents + return false; + } else { + return saveable.dirty; + } + } + + protected applySaveableWidget(widget: Widget, saveable: Saveable): void { + if (SaveableWidget.is(widget)) { + return; + } + const saveableWidget = widget as PostCreationSaveableWidget; + setDirty(saveableWidget, saveable.dirty); + saveable.onDirtyChanged(() => setDirty(saveableWidget, saveable.dirty)); + const closeWithSaving = this.createCloseWithSaving(); + const closeWithoutSaving = async () => { + const revert = Saveable.closingWidgetWouldLoseSaveable(saveableWidget, Array.from(this.saveThrottles.keys())); + await this.closeWithoutSaving(saveableWidget, revert); + }; + Object.assign(saveableWidget, { + closeWithoutSaving, + closeWithSaving, + close: closeWithSaving, + [close]: saveableWidget.close, + }); + } + + protected createCloseWithSaving(): (this: SaveableWidget, options?: SaveableWidget.CloseOptions) => Promise { + let closing = false; + const doSave = this.closeWithSaving.bind(this); + return async function (this: SaveableWidget, options?: SaveableWidget.CloseOptions): Promise { + if (closing) { + return; + } + closing = true; + try { + await doSave(this, options); + } finally { + closing = false; + } + }; + } + + protected async closeWithSaving(widget: PostCreationSaveableWidget, options?: SaveableWidget.CloseOptions): Promise { + const result = await this.shouldSaveWidget(widget, options); + if (typeof result === 'boolean') { + if (result) { + await this.save(widget, { + saveReason: SaveReason.AfterDelay + }); + if (!Saveable.isDirty(widget)) { + await widget.closeWithoutSaving(); + } + } else { + await widget.closeWithoutSaving(); + } + } + } + + protected async shouldSaveWidget(widget: PostCreationSaveableWidget, options?: SaveableWidget.CloseOptions): Promise { + if (!Saveable.isDirty(widget)) { + return false; + } + if (this.autoSave !== 'off') { + return true; + } + const notLastWithDocument = !Saveable.closingWidgetWouldLoseSaveable(widget, Array.from(this.saveThrottles.keys())); + if (notLastWithDocument) { + await widget.closeWithoutSaving(false); + return undefined; + } + if (options && options.shouldSave) { + return options.shouldSave(); + } + return new ShouldSaveDialog(widget).open(); + } + + protected async closeWithoutSaving(widget: PostCreationSaveableWidget, doRevert: boolean = true): Promise { + const saveable = Saveable.get(widget); + if (saveable && doRevert && saveable.dirty && saveable.revert) { + await saveable.revert(); + } + widget[close](); + return waitForClosed(widget); + } + + /** + * Indicate if the document can be saved ('Save' command should be disable if not). + */ + canSave(widget?: Widget): widget is Widget & (Saveable | SaveableSource) { + return Saveable.isDirty(widget) && (this.canSaveNotSaveAs(widget) || this.canSaveAs(widget)); + } + + canSaveNotSaveAs(widget?: Widget): widget is Widget & (Saveable | SaveableSource) { + // By default, we never allow a document to be saved if it is untitled. + return Boolean(widget && NavigatableWidget.getUri(widget)?.scheme !== UNTITLED_SCHEME); + } + + /** + * Saves the document + * + * No op if the widget is not saveable. + */ + async save(widget: Widget | undefined, options?: SaveOptions): Promise { + if (this.canSaveNotSaveAs(widget)) { + await Saveable.save(widget, options); + return NavigatableWidget.getUri(widget); + } else if (this.canSaveAs(widget)) { + return this.saveAs(widget, options); + } + } + + canSaveAs(saveable?: Widget): saveable is Widget & SaveableSource & Navigatable { + return false; + } + + saveAs(sourceWidget: Widget & SaveableSource & Navigatable, options?: SaveOptions): Promise { + return Promise.reject('Unsupported: The base SaveResourceService does not support saveAs action.'); + } +} + +export class AutoSaveThrottle implements Disposable { + + private _saveable: Saveable; + private _callback: () => void; + private _saveService: SaveableService; + private _disposable: DisposableCollection; + private _throttle?: ReturnType; + + constructor(saveable: Saveable, saveService: SaveableService, callback: () => void, ...disposables: Disposable[]) { + this._callback = callback; + this._saveable = saveable; + this._saveService = saveService; + this._disposable = new DisposableCollection( + ...disposables, + saveable.onContentChanged(() => { + this.throttledSave(); + }), + saveable.onDirtyChanged(() => { + this.throttledSave(); + }), + saveService.onDidAutoSaveChange(() => { + this.throttledSave(); + }), + saveService.onDidAutoSaveDelayChange(() => { + this.throttledSave(true); + }) + ); + } + + protected throttledSave(reset = false): void { + this._throttle?.cancel(); + if (reset) { + this._throttle = undefined; + } + if (this._saveService.autoSave === 'afterDelay' && this._saveable.dirty) { + if (!this._throttle) { + this._throttle = throttle(() => this._callback(), this._saveService.autoSaveDelay, { + leading: false, + trailing: true + }); + } + this._throttle(); + } + } + + dispose(): void { + this._disposable.dispose(); + } + +} diff --git a/packages/core/src/browser/saveable.ts b/packages/core/src/browser/saveable.ts index bfedeff9a19e6..fff0ae99a6297 100644 --- a/packages/core/src/browser/saveable.ts +++ b/packages/core/src/browser/saveable.ts @@ -20,14 +20,24 @@ import { Emitter, Event } from '../common/event'; import { MaybePromise } from '../common/types'; import { Key } from './keyboard/keys'; import { AbstractDialog } from './dialogs'; -import { waitForClosed } from './widgets'; import { nls } from '../common/nls'; -import { Disposable, isObject } from '../common'; +import { Disposable, DisposableCollection, isObject } from '../common'; +import { BinaryBuffer } from '../common/buffer'; + +export type AutoSaveMode = 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange'; export interface Saveable { readonly dirty: boolean; + /** + * This event is fired when the content of the `dirty` variable changes. + */ readonly onDirtyChanged: Event; - readonly autoSave: 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange'; + /** + * This event is fired when the content of the saveable changes. + * While `onDirtyChanged` is fired to notify the UI that the widget is dirty, + * `onContentChanged` is used for the auto save throttling. + */ + readonly onContentChanged: Event; /** * Saves dirty changes. */ @@ -37,13 +47,17 @@ export interface Saveable { */ revert?(options?: Saveable.RevertOptions): Promise; /** - * Creates a snapshot of the dirty state. + * Creates a snapshot of the dirty state. See also {@link Saveable.Snapshot}. */ createSnapshot?(): Saveable.Snapshot; /** * Applies the given snapshot to the dirty state. */ applySnapshot?(snapshot: object): void; + /** + * Serializes the full state of the saveable item to a binary buffer. + */ + serialize?(): Promise; } export interface SaveableSource { @@ -53,11 +67,15 @@ export interface SaveableSource { export class DelegatingSaveable implements Saveable { dirty = false; protected readonly onDirtyChangedEmitter = new Emitter(); + protected readonly onContentChangedEmitter = new Emitter(); get onDirtyChanged(): Event { return this.onDirtyChangedEmitter.event; } - autoSave: 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange' = 'off'; + + get onContentChanged(): Event { + return this.onContentChangedEmitter.event; + } async save(options?: SaveOptions): Promise { await this._delegate?.save(options); @@ -66,18 +84,22 @@ export class DelegatingSaveable implements Saveable { revert?(options?: Saveable.RevertOptions): Promise; createSnapshot?(): Saveable.Snapshot; applySnapshot?(snapshot: object): void; + serialize?(): Promise; protected _delegate?: Saveable; - protected toDispose?: Disposable; + protected toDispose = new DisposableCollection(); set delegate(delegate: Saveable) { - this.toDispose?.dispose(); + this.toDispose.dispose(); + this.toDispose = new DisposableCollection(); this._delegate = delegate; - this.toDispose = delegate.onDirtyChanged(() => { + this.toDispose.push(delegate.onDirtyChanged(() => { this.dirty = delegate.dirty; this.onDirtyChangedEmitter.fire(); - }); - this.autoSave = delegate.autoSave; + })); + this.toDispose.push(delegate.onContentChanged(() => { + this.onContentChangedEmitter.fire(); + })); if (this.dirty !== delegate.dirty) { this.dirty = delegate.dirty; this.onDirtyChangedEmitter.fire(); @@ -85,8 +107,77 @@ export class DelegatingSaveable implements Saveable { this.revert = delegate.revert?.bind(delegate); this.createSnapshot = delegate.createSnapshot?.bind(delegate); this.applySnapshot = delegate.applySnapshot?.bind(delegate); + this.serialize = delegate.serialize?.bind(delegate); + } + +} + +export class CompositeSaveable implements Saveable { + protected isDirty = false; + protected readonly onDirtyChangedEmitter = new Emitter(); + protected readonly onContentChangedEmitter = new Emitter(); + protected readonly toDispose = new DisposableCollection(this.onDirtyChangedEmitter, this.onContentChangedEmitter); + protected readonly saveablesMap = new Map(); + + get dirty(): boolean { + return this.isDirty; + } + + get onDirtyChanged(): Event { + return this.onDirtyChangedEmitter.event; + } + + get onContentChanged(): Event { + return this.onContentChangedEmitter.event; + } + + async save(options?: SaveOptions): Promise { + await Promise.all(this.saveables.map(saveable => saveable.save(options))); + } + + async revert(options?: Saveable.RevertOptions): Promise { + await Promise.all(this.saveables.map(saveable => saveable.revert?.(options))); + } + + get saveables(): readonly Saveable[] { + return Array.from(this.saveablesMap.keys()); + } + + add(saveable: Saveable): void { + if (this.saveablesMap.has(saveable)) { + return; + } + const toDispose = new DisposableCollection(); + this.toDispose.push(toDispose); + this.saveablesMap.set(saveable, toDispose); + toDispose.push(Disposable.create(() => { + this.saveablesMap.delete(saveable); + })); + toDispose.push(saveable.onDirtyChanged(() => { + const wasDirty = this.isDirty; + this.isDirty = this.saveables.some(s => s.dirty); + if (this.isDirty !== wasDirty) { + this.onDirtyChangedEmitter.fire(); + } + })); + toDispose.push(saveable.onContentChanged(() => { + this.onContentChangedEmitter.fire(); + })); + if (saveable.dirty && !this.isDirty) { + this.isDirty = true; + this.onDirtyChangedEmitter.fire(); + } + } + + remove(saveable: Saveable): boolean { + const toDispose = this.saveablesMap.get(saveable); + toDispose?.dispose(); + return !!toDispose; } + dispose(): void { + this.toDispose.dispose(); + } } export namespace Saveable { @@ -98,7 +189,16 @@ export namespace Saveable { soft?: boolean } + /** + * A snapshot of a saveable item. + * Applying a snapshot of a saveable on another (of the same type) using the `applySnapshot` should yield the state of the original saveable. + */ export type Snapshot = { value: string } | { read(): string | null }; + export namespace Snapshot { + export function read(snapshot: Snapshot): string | undefined { + return 'value' in snapshot ? snapshot.value : (snapshot.read() ?? undefined); + } + } export function isSource(arg: unknown): arg is SaveableSource { return isObject(arg) && is(arg.saveable); } @@ -131,52 +231,6 @@ export namespace Saveable { } } - async function closeWithoutSaving(this: PostCreationSaveableWidget, doRevert: boolean = true): Promise { - const saveable = get(this); - if (saveable && doRevert && saveable.dirty && saveable.revert) { - await saveable.revert(); - } - this[close](); - return waitForClosed(this); - } - - function createCloseWithSaving( - getOtherSaveables?: () => Array, - doSave?: (widget: Widget, options?: SaveOptions) => Promise - ): (this: SaveableWidget, options?: SaveableWidget.CloseOptions) => Promise { - let closing = false; - return async function (this: SaveableWidget, options: SaveableWidget.CloseOptions): Promise { - if (closing) { return; } - const saveable = get(this); - if (!saveable) { return; } - closing = true; - try { - const result = await shouldSave(saveable, () => { - const notLastWithDocument = !closingWidgetWouldLoseSaveable(this, getOtherSaveables?.() ?? []); - if (notLastWithDocument) { - return this.closeWithoutSaving(false).then(() => undefined); - } - if (options && options.shouldSave) { - return options.shouldSave(); - } - return new ShouldSaveDialog(this).open(); - }); - if (typeof result === 'boolean') { - if (result) { - await (doSave?.(this) ?? Saveable.save(this)); - if (!isDirty(this)) { - await this.closeWithoutSaving(); - } - } else { - await this.closeWithoutSaving(); - } - } - } finally { - closing = false; - } - }; - } - export async function confirmSaveBeforeClose(toClose: Iterable, others: Widget[]): Promise { for (const widget of toClose) { const saveable = Saveable.get(widget); @@ -197,49 +251,9 @@ export namespace Saveable { return true; } - /** - * @param widget the widget that may be closed - * @param others widgets that will not be closed. - * @returns `true` if widget is saveable and no widget among the `others` refers to the same saveable. `false` otherwise. - */ - function closingWidgetWouldLoseSaveable(widget: Widget, others: Widget[]): boolean { - const saveable = get(widget); - return !!saveable && !others.some(otherWidget => otherWidget !== widget && get(otherWidget) === saveable); - } - - export function apply( - widget: Widget, - getOtherSaveables?: () => Array, - doSave?: (widget: Widget, options?: SaveOptions) => Promise, - ): SaveableWidget | undefined { - if (SaveableWidget.is(widget)) { - return widget; - } + export function closingWidgetWouldLoseSaveable(widget: Widget, others: Widget[]): boolean { const saveable = Saveable.get(widget); - if (!saveable) { - return undefined; - } - const saveableWidget = widget as SaveableWidget; - setDirty(saveableWidget, saveable.dirty); - saveable.onDirtyChanged(() => setDirty(saveableWidget, saveable.dirty)); - const closeWithSaving = createCloseWithSaving(getOtherSaveables, doSave); - return Object.assign(saveableWidget, { - closeWithoutSaving, - closeWithSaving, - close: closeWithSaving, - [close]: saveableWidget.close, - }); - } - export async function shouldSave(saveable: Saveable, cb: () => MaybePromise): Promise { - if (!saveable.dirty) { - return false; - } - - if (saveable.autoSave !== 'off') { - return true; - } - - return cb(); + return !!saveable && !others.some(otherWidget => otherWidget !== widget && Saveable.get(otherWidget) === saveable); } } @@ -302,11 +316,27 @@ export const enum FormatType { DIRTY }; +export enum SaveReason { + Manual = 1, + AfterDelay = 2, + FocusChange = 3 +} + +export namespace SaveReason { + export function isManual(reason?: number): reason is typeof SaveReason.Manual { + return reason === SaveReason.Manual; + } +} + export interface SaveOptions { /** * Formatting type to apply when saving. */ readonly formatType?: FormatType; + /** + * The reason for saving the resource. + */ + readonly saveReason?: SaveReason; } /** diff --git a/packages/core/src/browser/secondary-window-handler.ts b/packages/core/src/browser/secondary-window-handler.ts index d5aaef2c30a17..e967da357b1e5 100644 --- a/packages/core/src/browser/secondary-window-handler.ts +++ b/packages/core/src/browser/secondary-window-handler.ts @@ -22,8 +22,6 @@ import { ApplicationShell } from './shell/application-shell'; import { Emitter } from '../common/event'; import { SecondaryWindowService } from './window/secondary-window-service'; import { KeybindingRegistry } from './keybinding'; -import { ColorApplicationContribution } from './color-application-contribution'; -import { StylingService } from './styling-service'; /** Widget to be contained directly in a secondary window. */ class SecondaryWindowRootWidget extends Widget { @@ -47,7 +45,6 @@ class SecondaryWindowRootWidget extends Widget { * This handler manages the opened secondary windows and sets up messaging between them and the Theia main window. * In addition, it provides access to the extracted widgets and provides notifications when widgets are added to or removed from this handler. * - * @experimental The functionality provided by this handler is experimental and has known issues in Electron apps. */ @injectable() export class SecondaryWindowHandler { @@ -59,17 +56,19 @@ export class SecondaryWindowHandler { @inject(KeybindingRegistry) protected keybindings: KeybindingRegistry; - @inject(ColorApplicationContribution) - protected colorAppContribution: ColorApplicationContribution; - - @inject(StylingService) - protected stylingService: StylingService; + protected readonly onWillAddWidgetEmitter = new Emitter<[Widget, Window]>(); + /** Subscribe to get notified when a widget is added to this handler, i.e. the widget was moved to an secondary window . */ + readonly onWillAddWidget = this.onWillAddWidgetEmitter.event; - protected readonly onDidAddWidgetEmitter = new Emitter(); + protected readonly onDidAddWidgetEmitter = new Emitter<[Widget, Window]>(); /** Subscribe to get notified when a widget is added to this handler, i.e. the widget was moved to an secondary window . */ readonly onDidAddWidget = this.onDidAddWidgetEmitter.event; - protected readonly onDidRemoveWidgetEmitter = new Emitter(); + protected readonly onWillRemoveWidgetEmitter = new Emitter<[Widget, Window]>(); + /** Subscribe to get notified when a widget is removed from this handler, i.e. the widget's window was closed or the widget was disposed. */ + readonly onWillRemoveWidget = this.onWillRemoveWidgetEmitter.event; + + protected readonly onDidRemoveWidgetEmitter = new Emitter<[Widget, Window]>(); /** Subscribe to get notified when a widget is removed from this handler, i.e. the widget's window was closed or the widget was disposed. */ readonly onDidRemoveWidget = this.onDidRemoveWidgetEmitter.event; @@ -122,7 +121,8 @@ export class SecondaryWindowHandler { } const mainWindowTitle = document.title; - newWindow.onload = () => { + + newWindow.addEventListener('load', () => { this.keybindings.registerEventListeners(newWindow); // Use the widget's title as the window title // Even if the widget's label were malicious, this should be safe against XSS because the HTML standard defines this is inserted via a text node. @@ -134,8 +134,8 @@ export class SecondaryWindowHandler { console.error('Could not find dom element to attach to in secondary window'); return; } - const unregisterWithColorContribution = this.colorAppContribution.registerWindow(newWindow); - const unregisterWithStylingService = this.stylingService.registerWindow(newWindow); + + this.onWillAddWidgetEmitter.fire([widget, newWindow]); widget.secondaryWindow = newWindow; const rootWidget = new SecondaryWindowRootWidget(); @@ -145,13 +145,12 @@ export class SecondaryWindowHandler { widget.show(); widget.update(); - this.addWidget(widget); + this.addWidget(widget, newWindow); // Close the window if the widget is disposed, e.g. by a command closing all widgets. widget.disposed.connect(() => { - unregisterWithColorContribution.dispose(); - unregisterWithStylingService.dispose(); - this.removeWidget(widget); + this.onWillRemoveWidgetEmitter.fire([widget, newWindow]); + this.removeWidget(widget, newWindow); if (!newWindow.closed) { newWindow.close(); } @@ -165,7 +164,7 @@ export class SecondaryWindowHandler { updateWidget(); }); widget.activate(); - }; + }); } /** @@ -195,18 +194,18 @@ export class SecondaryWindowHandler { return undefined; } - protected addWidget(widget: ExtractableWidget): void { + protected addWidget(widget: ExtractableWidget, win: Window): void { if (!this._widgets.includes(widget)) { this._widgets.push(widget); - this.onDidAddWidgetEmitter.fire(widget); + this.onDidAddWidgetEmitter.fire([widget, win]); } } - protected removeWidget(widget: ExtractableWidget): void { + protected removeWidget(widget: ExtractableWidget, win: Window): void { const index = this._widgets.indexOf(widget); if (index > -1) { this._widgets.splice(index, 1); - this.onDidRemoveWidgetEmitter.fire(widget); + this.onDidRemoveWidgetEmitter.fire([widget, win]); } } } diff --git a/packages/core/src/browser/shell/additional-views-menu-widget.tsx b/packages/core/src/browser/shell/additional-views-menu-widget.tsx index 5a4f0bb9b9f15..05f57919faf30 100644 --- a/packages/core/src/browser/shell/additional-views-menu-widget.tsx +++ b/packages/core/src/browser/shell/additional-views-menu-widget.tsx @@ -23,13 +23,13 @@ import { SideTabBar } from './tab-bars'; export const AdditionalViewsMenuWidgetFactory = Symbol('AdditionalViewsMenuWidgetFactory'); export type AdditionalViewsMenuWidgetFactory = (side: 'left' | 'right') => AdditionalViewsMenuWidget; -export const ADDITIONAL_VIEWS_MENU_PATH: MenuPath = ['additional_views_menu']; - +export const AdditionalViewsMenuPath = Symbol('AdditionalViewsMenuPath'); @injectable() export class AdditionalViewsMenuWidget extends SidebarMenuWidget { static readonly ID = 'sidebar.additional.views'; - side: 'left' | 'right'; + @inject(AdditionalViewsMenuPath) + protected menuPath: MenuPath; @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @@ -47,7 +47,7 @@ export class AdditionalViewsMenuWidget extends SidebarMenuWidget { title: nls.localizeByDefault('Additional Views'), iconClass: codicon('ellipsis'), id: AdditionalViewsMenuWidget.ID, - menuPath: ADDITIONAL_VIEWS_MENU_PATH, + menuPath: this.menuPath, order: 0 }); } @@ -66,6 +66,6 @@ export class AdditionalViewsMenuWidget extends SidebarMenuWidget { }); } })); - this.menuDisposables.push(this.menuModelRegistry.registerMenuAction(ADDITIONAL_VIEWS_MENU_PATH, { commandId: command.id, order: index.toString() })); + this.menuDisposables.push(this.menuModelRegistry.registerMenuAction(this.menuPath, { commandId: command.id, order: index.toString() })); } } diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index b93858eccdb54..b13081abadb88 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -24,7 +24,7 @@ import { Message } from '@phosphor/messaging'; import { IDragEvent } from '@phosphor/dragdrop'; import { RecursivePartial, Event as CommonEvent, DisposableCollection, Disposable, environment, isObject } from '../../common'; import { animationFrame } from '../browser'; -import { Saveable, SaveableWidget, SaveOptions, SaveableSource } from '../saveable'; +import { Saveable, SaveableWidget, SaveOptions } from '../saveable'; import { StatusBarImpl, StatusBarEntry, StatusBarAlignment } from '../status-bar/status-bar'; import { TheiaDockPanel, BOTTOM_AREA_ID, MAIN_AREA_ID } from './theia-dock-panel'; import { SidePanelHandler, SidePanel, SidePanelHandlerFactory } from './side-panel-handler'; @@ -38,12 +38,13 @@ import { waitForRevealed, waitForClosed, PINNED_CLASS } from '../widgets'; import { CorePreferences } from '../core-preferences'; import { BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer'; import { Deferred } from '../../common/promise-util'; -import { SaveResourceService } from '../save-resource-service'; +import { SaveableService } from '../saveable-service'; import { nls } from '../../common/nls'; import { SecondaryWindowHandler } from '../secondary-window-handler'; import URI from '../../common/uri'; import { OpenerService } from '../opener-service'; import { PreviewableWidget } from '../widgets/previewable-widget'; +import { WindowService } from '../window/window-service'; /** The class name added to ApplicationShell instances. */ const APPLICATION_SHELL_CLASS = 'theia-ApplicationShell'; @@ -132,16 +133,19 @@ export class DockPanelRenderer implements DockLayout.IRenderer { getDynamicTabOptions()); this.tabBarClasses.forEach(c => tabBar.addClass(c)); renderer.tabBar = tabBar; - tabBar.disposed.connect(() => renderer.dispose()); renderer.contextMenuPath = SHELL_TABBAR_CONTEXT_MENU; tabBar.currentChanged.connect(this.onCurrentTabChanged, this); - this.corePreferences.onPreferenceChanged(change => { + const prefChangeDisposable = this.corePreferences.onPreferenceChanged(change => { if (change.preferenceName === 'workbench.tab.shrinkToFit.enabled' || change.preferenceName === 'workbench.tab.shrinkToFit.minimumSize' || change.preferenceName === 'workbench.tab.shrinkToFit.defaultSize') { tabBar.dynamicTabOptions = getDynamicTabOptions(); } }); + tabBar.disposed.connect(() => { + prefChangeDisposable.dispose(); + renderer.dispose(); + }); this.onDidCreateTabBarEmitter.fire(tabBar); return tabBar; } @@ -268,8 +272,9 @@ export class ApplicationShell extends Widget { @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService, @inject(ApplicationShellOptions) @optional() options: RecursivePartial = {}, @inject(CorePreferences) protected readonly corePreferences: CorePreferences, - @inject(SaveResourceService) protected readonly saveResourceService: SaveResourceService, + @inject(SaveableService) protected readonly saveableService: SaveableService, @inject(SecondaryWindowHandler) protected readonly secondaryWindowHandler: SecondaryWindowHandler, + @inject(WindowService) protected readonly windowService: WindowService ) { super(options as Widget.IOptions); @@ -335,8 +340,8 @@ export class ApplicationShell extends Widget { this.rightPanelHandler.dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget)); this.secondaryWindowHandler.init(this); - this.secondaryWindowHandler.onDidAddWidget(widget => this.fireDidAddWidget(widget)); - this.secondaryWindowHandler.onDidRemoveWidget(widget => this.fireDidRemoveWidget(widget)); + this.secondaryWindowHandler.onDidAddWidget(([widget, window]) => this.fireDidAddWidget(widget)); + this.secondaryWindowHandler.onDidRemoveWidget(([widget, window]) => this.fireDidRemoveWidget(widget)); this.layout = this.createLayout(); @@ -955,7 +960,7 @@ export class ApplicationShell extends Widget { } } - getInsertionOptions(options?: Readonly): { area: string; addOptions: DockLayout.IAddOptions; } { + getInsertionOptions(options?: Readonly): { area: string; addOptions: TheiaDockPanel.AddOptions; } { let ref: Widget | undefined = options?.ref; let area: ApplicationShell.Area = options?.area || 'main'; if (!ref && (area === 'main' || area === 'bottom')) { @@ -964,7 +969,7 @@ export class ApplicationShell extends Widget { } // make sure that ref belongs to area area = ref && this.getAreaFor(ref) || area; - const addOptions: DockPanel.IAddOptions = {}; + const addOptions: TheiaDockPanel.AddOptions = {}; if (ApplicationShell.isOpenToSideMode(options?.mode)) { const areaPanel = area === 'main' ? this.mainPanel : area === 'bottom' ? this.bottomPanel : undefined; const sideRef = areaPanel && ref && (options?.mode === 'open-to-left' ? @@ -976,6 +981,10 @@ export class ApplicationShell extends Widget { addOptions.ref = ref; addOptions.mode = options?.mode === 'open-to-left' ? 'split-left' : 'split-right'; } + } else if (ApplicationShell.isReplaceMode(options?.mode)) { + addOptions.ref = options?.ref; + addOptions.closeRef = true; + addOptions.mode = 'tab-after'; } else { addOptions.ref = ref; addOptions.mode = options?.mode; @@ -1226,11 +1235,6 @@ export class ApplicationShell extends Widget { } this.tracker.add(widget); this.checkActivation(widget); - Saveable.apply( - widget, - () => this.widgets.filter((maybeSaveable): maybeSaveable is Widget & SaveableSource => !!Saveable.get(maybeSaveable)), - (toSave, options) => this.saveResourceService.save(toSave, options), - ); if (ApplicationShell.TrackableWidgetProvider.is(widget)) { for (const toTrack of widget.getTrackableWidgets()) { this.track(toTrack); @@ -1318,20 +1322,23 @@ export class ApplicationShell extends Widget { let widget = find(this.mainPanel.widgets(), w => w.id === id); if (widget) { this.mainPanel.activateWidget(widget); - return widget; } - widget = find(this.bottomPanel.widgets(), w => w.id === id); - if (widget) { - this.expandBottomPanel(); - this.bottomPanel.activateWidget(widget); - return widget; + if (!widget) { + widget = find(this.bottomPanel.widgets(), w => w.id === id); + if (widget) { + this.expandBottomPanel(); + this.bottomPanel.activateWidget(widget); + } } - widget = this.leftPanelHandler.activate(id); - if (widget) { - return widget; + if (!widget) { + widget = this.leftPanelHandler.activate(id); + } + + if (!widget) { + widget = this.rightPanelHandler.activate(id); } - widget = this.rightPanelHandler.activate(id); if (widget) { + this.windowService.focus(); return widget; } return this.secondaryWindowHandler.activateWidget(id); @@ -1428,17 +1435,19 @@ export class ApplicationShell extends Widget { if (tabBar) { tabBar.currentTitle = widget.title; } - return widget; } - widget = this.leftPanelHandler.expand(id); - if (widget) { - return widget; + if (!widget) { + widget = this.leftPanelHandler.expand(id); + } + if (!widget) { + widget = this.rightPanelHandler.expand(id); } - widget = this.rightPanelHandler.expand(id); if (widget) { + this.windowService.focus(); return widget; + } else { + return this.secondaryWindowHandler.revealWidget(id); } - return this.secondaryWindowHandler.revealWidget(id); } /** @@ -1887,6 +1896,10 @@ export class ApplicationShell extends Widget { if (index < current.titles.length - 1) { return index + 1; } + // last item in tab bar. select the previous one. + if (index === current.titles.length - 1) { + return index - 1; + } return 0; } @@ -2031,21 +2044,21 @@ export class ApplicationShell extends Widget { * Test whether the current widget is dirty. */ canSave(): boolean { - return this.saveResourceService.canSave(this.currentWidget); + return this.saveableService.canSave(this.currentWidget); } /** * Save the current widget if it is dirty. */ async save(options?: SaveOptions): Promise { - await this.saveResourceService.save(this.currentWidget, options); + await this.saveableService.save(this.currentWidget, options); } /** * Test whether there is a dirty widget. */ canSaveAll(): boolean { - return this.tracker.widgets.some(widget => this.saveResourceService.canSave(widget)); + return this.tracker.widgets.some(widget => this.saveableService.canSave(widget)); } /** @@ -2053,8 +2066,8 @@ export class ApplicationShell extends Widget { */ async saveAll(options?: SaveOptions): Promise { for (const widget of this.widgets) { - if (this.saveResourceService.canSaveNotSaveAs(widget)) { - await this.saveResourceService.save(widget, options); + if (Saveable.isDirty(widget) && this.saveableService.canSaveNotSaveAs(widget)) { + await this.saveableService.save(widget, options); } } } @@ -2101,7 +2114,7 @@ export namespace ApplicationShell { export const areaLabels: Record = { main: nls.localizeByDefault('Main'), - top: nls.localize('theia/shell-area/top', 'Top'), + top: nls.localizeByDefault('Top'), left: nls.localizeByDefault('Left'), right: nls.localizeByDefault('Right'), bottom: nls.localizeByDefault('Bottom'), @@ -2167,6 +2180,15 @@ export namespace ApplicationShell { return mode === 'open-to-left' || mode === 'open-to-right'; } + /** + * Whether the `ref` of the options widget should be replaced. + */ + export type ReplaceMode = 'tab-replace'; + + export function isReplaceMode(mode: unknown): mode is ReplaceMode { + return mode === 'tab-replace'; + } + /** * Options for adding a widget to the application shell. */ @@ -2180,7 +2202,7 @@ export namespace ApplicationShell { * * The default is `'tab-after'`. */ - mode?: DockLayout.InsertMode | OpenToSideMode + mode?: DockLayout.InsertMode | OpenToSideMode | ReplaceMode /** * The reference widget for the insert location. * diff --git a/packages/core/src/browser/shell/side-panel-handler.ts b/packages/core/src/browser/shell/side-panel-handler.ts index 6bfdb5e066486..ec38f8f2201ff 100644 --- a/packages/core/src/browser/shell/side-panel-handler.ts +++ b/packages/core/src/browser/shell/side-panel-handler.ts @@ -211,6 +211,7 @@ export class SidePanelHandler { protected createAdditionalViewsWidget(): AdditionalViewsMenuWidget { const widget = this.additionalViewsMenuFactory(this.side); widget.addClass('theia-sidebar-menu'); + widget.addClass('theia-additional-views-menu'); return widget; } @@ -653,7 +654,7 @@ export class SidePanelHandler { } protected onTabsOverflowChanged(sender: SideTabBar, event: { titles: Title[], startIndex: number }): void { - if (event.startIndex >= 0 && event.startIndex <= sender.currentIndex) { + if (event.startIndex > 0 && event.startIndex <= sender.currentIndex) { sender.revealTab(sender.currentIndex); } else { this.additionalViewsMenu.updateAdditionalViews(sender, event); diff --git a/packages/core/src/browser/shell/sidebar-menu-widget.tsx b/packages/core/src/browser/shell/sidebar-menu-widget.tsx index 8e75b50fff5d4..b2faa89240fa9 100644 --- a/packages/core/src/browser/shell/sidebar-menu-widget.tsx +++ b/packages/core/src/browser/shell/sidebar-menu-widget.tsx @@ -20,6 +20,7 @@ import { ReactWidget } from '../widgets'; import { ContextMenuRenderer } from '../context-menu-renderer'; import { MenuPath } from '../../common/menu'; import { HoverService } from '../hover-service'; +import { Event, Disposable, Emitter, DisposableCollection } from '../../common'; export const SidebarTopMenuWidgetFactory = Symbol('SidebarTopMenuWidgetFactory'); export const SidebarBottomMenuWidgetFactory = Symbol('SidebarBottomMenuWidgetFactory'); @@ -29,18 +30,54 @@ export interface SidebarMenu { iconClass: string; title: string; menuPath: MenuPath; + onDidBadgeChange?: Event; /* * Used to sort menus. The lower the value the lower they are placed in the sidebar. */ order: number; } +export class SidebarMenuItem implements Disposable { + + readonly menu: SidebarMenu; + get badge(): string { + if (this._badge <= 0) { + return ''; + } else if (this._badge > 99) { + return '99+'; + } else { + return this._badge.toString(); + } + }; + protected readonly onDidBadgeChangeEmitter = new Emitter(); + readonly onDidBadgeChange: Event = this.onDidBadgeChangeEmitter.event; + protected _badge = 0; + + protected readonly toDispose = new DisposableCollection(); + + constructor(menu: SidebarMenu) { + this.menu = menu; + if (menu.onDidBadgeChange) { + this.toDispose.push(menu.onDidBadgeChange(value => { + this._badge = value; + this.onDidBadgeChangeEmitter.fire(value); + })); + } + } + + dispose(): void { + this.toDispose.dispose(); + this.onDidBadgeChangeEmitter.dispose(); + } + +} + /** * The menu widget placed on the sidebar. */ @injectable() export class SidebarMenuWidget extends ReactWidget { - protected readonly menus: SidebarMenu[]; + protected readonly items: SidebarMenuItem[]; /** * The element that had focus when a menu rendered by this widget was activated. */ @@ -58,27 +95,27 @@ export class SidebarMenuWidget extends ReactWidget { constructor() { super(); - this.menus = []; + this.items = []; } addMenu(menu: SidebarMenu): void { - const exists = this.menus.find(m => m.id === menu.id); + const exists = this.items.find(item => item.menu.id === menu.id); if (exists) { return; } - this.menus.push(menu); - this.menus.sort((a, b) => a.order - b.order); + const newItem = new SidebarMenuItem(menu); + newItem.onDidBadgeChange(() => this.update()); + this.items.push(newItem); + this.items.sort((a, b) => a.menu.order - b.menu.order); this.update(); } removeMenu(menuId: string): void { - const menu = this.menus.find(m => m.id === menuId); - if (menu) { - const index = this.menus.indexOf(menu); - if (index !== -1) { - this.menus.splice(index, 1); - this.update(); - } + const index = this.items.findIndex(m => m.menu.id === menuId); + if (index !== -1) { + this.items[index].dispose(); + this.items.splice(index, 1); + this.update(); } } @@ -127,14 +164,20 @@ export class SidebarMenuWidget extends ReactWidget { protected render(): React.ReactNode { return - {this.menus.map(menu => this.onClick(e, menu.menuPath)} - onMouseDown={this.onMouseDown} - onMouseEnter={e => this.onMouseEnter(e, menu.title)} - onMouseLeave={this.onMouseOut} - />)} + {this.items.map(item => this.renderItem(item))} ; } + + protected renderItem(item: SidebarMenuItem): React.ReactNode { + return
    this.onClick(e, item.menu.menuPath)} + onMouseDown={this.onMouseDown} + onMouseEnter={e => this.onMouseEnter(e, item.menu.title)} + onMouseLeave={this.onMouseOut}> + + {item.badge &&
    {item.badge}
    } +
    ; + } } diff --git a/packages/core/src/browser/shell/split-panels.ts b/packages/core/src/browser/shell/split-panels.ts index cb3de57c26dd7..d9f59bdf4a97a 100644 --- a/packages/core/src/browser/shell/split-panels.ts +++ b/packages/core/src/browser/shell/split-panels.ts @@ -91,13 +91,14 @@ export class SplitPositionHandler { move.resolve = resolve; move.reject = reject; if (this.splitMoves.length === 0) { - window.requestAnimationFrame(this.animationFrame.bind(this)); + setTimeout(this.animationFrame.bind(this), 10); } this.splitMoves.push(move); }); } - protected animationFrame(time: number): void { + protected animationFrame(): void { + const time = Date.now(); const move = this.splitMoves[this.currentMoveIndex]; let rejectedOrResolved = false; if (move.ended || move.referenceWidget && move.referenceWidget.isHidden) { @@ -133,7 +134,7 @@ export class SplitPositionHandler { this.currentMoveIndex = 0; } if (this.splitMoves.length > 0) { - window.requestAnimationFrame(this.animationFrame.bind(this)); + setTimeout(this.animationFrame.bind(this)); } } diff --git a/packages/core/src/browser/shell/tab-bar-decorator.ts b/packages/core/src/browser/shell/tab-bar-decorator.ts index 4b48d848876b1..7b9d6a9756e9a 100644 --- a/packages/core/src/browser/shell/tab-bar-decorator.ts +++ b/packages/core/src/browser/shell/tab-bar-decorator.ts @@ -17,9 +17,12 @@ import debounce = require('lodash.debounce'); import { Title, Widget } from '@phosphor/widgets'; import { inject, injectable, named } from 'inversify'; -import { Event, Emitter, ContributionProvider } from '../../common'; -import { WidgetDecoration } from '../widget-decoration'; +import { ContributionProvider, Emitter, Event } from '../../common'; +import { ColorRegistry } from '../color-registry'; +import { Decoration, DecorationsService, DecorationsServiceImpl } from '../decorations-service'; import { FrontendApplicationContribution } from '../frontend-application-contribution'; +import { Navigatable } from '../navigatable-types'; +import { WidgetDecoration } from '../widget-decoration'; export const TabBarDecorator = Symbol('TabBarDecorator'); @@ -53,6 +56,12 @@ export class TabBarDecoratorService implements FrontendApplicationContribution { @inject(ContributionProvider) @named(TabBarDecorator) protected readonly contributions: ContributionProvider; + @inject(DecorationsService) + protected readonly decorationsService: DecorationsServiceImpl; + + @inject(ColorRegistry) + protected readonly colors: ColorRegistry; + initialize(): void { this.contributions.getContributions().map(decorator => decorator.onDidChangeDecorations(this.fireDidChangeDecorations)); } @@ -66,11 +75,32 @@ export class TabBarDecoratorService implements FrontendApplicationContribution { */ getDecorations(title: Title): WidgetDecoration.Data[] { const decorators = this.contributions.getContributions(); - let all: WidgetDecoration.Data[] = []; + const decorations: WidgetDecoration.Data[] = []; for (const decorator of decorators) { - const decorations = decorator.decorate(title); - all = all.concat(decorations); + decorations.push(...decorator.decorate(title)); } - return all; + if (Navigatable.is(title.owner)) { + const resourceUri = title.owner.getResourceUri(); + if (resourceUri) { + const serviceDecorations = this.decorationsService.getDecoration(resourceUri, false); + decorations.push(...serviceDecorations.map(d => this.fromDecoration(d))); + } + } + return decorations; + } + + protected fromDecoration(decoration: Decoration): WidgetDecoration.Data { + const colorVariable = decoration.colorId && this.colors.toCssVariableName(decoration.colorId); + return { + tailDecorations: [ + { + data: decoration.letter ? decoration.letter : '', + fontData: { + color: colorVariable && `var(${colorVariable})` + }, + tooltip: decoration.tooltip ? decoration.tooltip : '' + } + ] + }; } } diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts index 1af64ee75bc75..261fbd4bbf9f5 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts @@ -15,12 +15,12 @@ // ***************************************************************************** import { MenuNode, MenuPath } from '../../../common'; -import { NAVIGATION, TabBarToolbarItem } from './tab-bar-toolbar-types'; +import { NAVIGATION, RenderedToolbarItem } from './tab-bar-toolbar-types'; export const TOOLBAR_WRAPPER_ID_SUFFIX = '-as-tabbar-toolbar-item'; -export class ToolbarMenuNodeWrapper implements TabBarToolbarItem { - constructor(protected readonly menuNode: MenuNode, readonly group?: string, readonly menuPath?: MenuPath) { } +export class ToolbarMenuNodeWrapper implements RenderedToolbarItem { + constructor(protected readonly menuNode: MenuNode, readonly group: string | undefined, readonly delegateMenuPath: MenuPath, readonly menuPath?: MenuPath) { } get id(): string { return this.menuNode.id + TOOLBAR_WRAPPER_ID_SUFFIX; } get command(): string { return this.menuNode.command ?? ''; }; get icon(): string | undefined { return this.menuNode.icon; } diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts index e3946be66d197..e10afb4a0c09e 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts @@ -17,11 +17,11 @@ import debounce = require('lodash.debounce'); import { inject, injectable, named } from 'inversify'; // eslint-disable-next-line max-len -import { CommandMenuNode, CommandRegistry, CompoundMenuNode, ContributionProvider, Disposable, DisposableCollection, Emitter, Event, MenuModelRegistry, MenuNode, MenuPath } from '../../../common'; +import { CommandRegistry, ContributionProvider, Disposable, DisposableCollection, Emitter, Event, MenuModelRegistry, MenuNode, MenuPath } from '../../../common'; import { ContextKeyService } from '../../context-key-service'; import { FrontendApplicationContribution } from '../../frontend-application-contribution'; import { Widget } from '../../widgets'; -import { AnyToolbarItem, ConditionalToolbarItem, MenuDelegate, MenuToolbarItem, ReactTabBarToolbarItem, TabBarToolbarItem } from './tab-bar-toolbar-types'; +import { MenuDelegate, ReactTabBarToolbarItem, RenderedToolbarItem, TabBarToolbarItem } from './tab-bar-toolbar-types'; import { ToolbarMenuNodeWrapper } from './tab-bar-toolbar-menu-adapters'; /** @@ -75,7 +75,7 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { * * @param item the item to register. */ - registerItem(item: TabBarToolbarItem | ReactTabBarToolbarItem): Disposable { + registerItem(item: RenderedToolbarItem | ReactTabBarToolbarItem): Disposable { const { id } = item; if (this.items.has(id)) { throw new Error(`A toolbar item is already registered with the '${id}' ID.`); @@ -110,24 +110,18 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { for (const delegate of this.menuDelegates.values()) { if (delegate.isVisible(widget)) { const menu = this.menuRegistry.getMenu(delegate.menuPath); - const children = CompoundMenuNode.getFlatChildren(menu.children); - for (const child of children) { + for (const child of menu.children) { if (!child.when || this.contextKeyService.match(child.when, widget.node)) { if (child.children) { for (const grandchild of child.children) { if (!grandchild.when || this.contextKeyService.match(grandchild.when, widget.node)) { - if (CommandMenuNode.is(grandchild)) { - result.push(new ToolbarMenuNodeWrapper(grandchild, child.id, delegate.menuPath)); - } else if (CompoundMenuNode.is(grandchild)) { - let menuPath; - if (menuPath = this.menuRegistry.getPath(grandchild)) { - result.push(new ToolbarMenuNodeWrapper(grandchild, child.id, menuPath)); - } - } + const menuPath = this.menuRegistry.getPath(grandchild); + result.push(new ToolbarMenuNodeWrapper(grandchild, child.id, delegate.menuPath, menuPath)); } } } else if (child.command) { - result.push(new ToolbarMenuNodeWrapper(child, '', delegate.menuPath)); + const menuPath = this.menuRegistry.getPath(child); + result.push(new ToolbarMenuNodeWrapper(child, undefined, delegate.menuPath, menuPath)); } } } @@ -145,15 +139,17 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { * @returns `false` if the `item` should be suppressed, otherwise `true` */ protected isItemVisible(item: TabBarToolbarItem | ReactTabBarToolbarItem, widget: Widget): boolean { - if (TabBarToolbarItem.is(item) && item.command && !this.isTabBarToolbarItemVisible(item, widget)) { + if (!this.isConditionalItemVisible(item, widget)) { return false; } - if (MenuToolbarItem.is(item) && !this.isMenuToolbarItemVisible(item, widget)) { + + if (item.command && !this.commandRegistry.isVisible(item.command, widget)) { return false; } - if (AnyToolbarItem.isConditional(item) && !this.isConditionalItemVisible(item, widget)) { + if (item.menuPath && !this.isNonEmptyMenu(item, widget)) { return false; } + // The item is not vetoed. Accept it return true; } @@ -166,7 +162,7 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { * @param widget the widget that is updating the toolbar * @returns `false` if the `item` should be suppressed, otherwise `true` */ - protected isConditionalItemVisible(item: ConditionalToolbarItem, widget: Widget): boolean { + protected isConditionalItemVisible(item: TabBarToolbarItem, widget: Widget): boolean { if (item.isVisible && !item.isVisible(widget)) { return false; } @@ -176,19 +172,6 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { return true; } - /** - * Query whether a tab-bar toolbar `item` that has a command should be shown in the toolbar. - * This implementation returns `false` if the `item`'s command is not visible in the - * `widget` according to the command registry. - * - * @param item a tab-bar toolbar item that has a non-empty `command` - * @param widget the widget that is updating the toolbar - * @returns `false` if the `item` should be suppressed, otherwise `true` - */ - protected isTabBarToolbarItemVisible(item: TabBarToolbarItem, widget: Widget): boolean { - return this.commandRegistry.isVisible(item.command, widget); - } - /** * Query whether a menu toolbar `item` should be shown in the toolbar. * This implementation returns `false` if the `item` does not have any actual menu to show. @@ -197,7 +180,10 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { * @param widget the widget that is updating the toolbar * @returns `false` if the `item` should be suppressed, otherwise `true` */ - protected isMenuToolbarItemVisible(item: MenuToolbarItem, widget: Widget): boolean { + isNonEmptyMenu(item: TabBarToolbarItem, widget: Widget | undefined): boolean { + if (!item.menuPath) { + return false; + } const menu = this.menuRegistry.getMenu(item.menuPath); const isVisible: (node: MenuNode) => boolean = node => node.children?.length @@ -220,7 +206,7 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { } } - registerMenuDelegate(menuPath: MenuPath, when?: string | ((widget: Widget) => boolean)): Disposable { + registerMenuDelegate(menuPath: MenuPath, when?: ((widget: Widget) => boolean)): Disposable { const id = this.toElementId(menuPath); if (!this.menuDelegates.has(id)) { const isVisible: MenuDelegate['isVisible'] = !when diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.ts b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.ts index 00ad879b4d761..c9db6e3b18027 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.ts +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import * as React from 'react'; -import { ArrayUtils, Event, isFunction, isObject, isString, MenuPath } from '../../../common'; +import { ArrayUtils, Event, isFunction, isObject, MenuPath } from '../../../common'; import { Widget } from '../../widgets'; /** Items whose group is exactly 'navigation' will be rendered inline. */ @@ -32,58 +32,21 @@ export namespace TabBarDelegator { } } -interface RegisteredToolbarItem { +export type TabBarToolbarItem = RenderedToolbarItem | ReactTabBarToolbarItem; + +/** + * Representation of an item in the tab + */ +export interface TabBarToolbarItemBase { /** * The unique ID of the toolbar item. */ id: string; -} - -interface RenderedToolbarItem { - /** - * Optional icon for the item. - */ - icon?: string | (() => string); - - /** - * Optional text of the item. - * - * Strings in the format `$(iconIdentifier~animationType) will be treated as icon references. - * If the iconIdentifier begins with fa-, Font Awesome icons will be used; otherwise it will be treated as Codicon name. - * - * You can find Codicon classnames here: https://microsoft.github.io/vscode-codicons/dist/codicon.html - * You can find Font Awesome classnames here: http://fontawesome.io/icons/ - * The type of animation can be either `spin` or `pulse`. - */ - text?: string; - - /** - * Optional tooltip for the item. - */ - tooltip?: string; -} - -interface SelfRenderingToolbarItem { - render(widget?: Widget): React.ReactNode; -} - -interface ExecutableToolbarItem { /** * The command to execute when the item is selected. */ - command: string; -} + command?: string; -export interface MenuToolbarItem { - /** - * A menu path with which this item is associated. - * If accompanied by a command, this data will be passed to the {@link MenuCommandExecutor}. - * If no command is present, this menu will be opened. - */ - menuPath: MenuPath; -} - -export interface ConditionalToolbarItem { /** * https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts */ @@ -98,61 +61,71 @@ export interface ConditionalToolbarItem { * Note: currently, each item of the container toolbar will be re-rendered if any of the items have changed. */ onDidChange?: Event; -} -interface InlineToolbarItemMetadata { /** * 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; - group: 'navigation' | undefined; -} - -interface MenuToolbarItemMetadata { + group?: string; /** - * Optional group for the item. Default `navigation`. - * `navigation` group will be inlined, while all the others will appear in the `...` dropdown. - * A group in format `submenu_group_1/submenu 1/.../submenu_group_n/ submenu n/item_group` means that the item will be located in a submenu(s) of the `...` dropdown. - * The submenu's title is named by the submenu section name, e.g. `group//subgroup`. + * A menu path with which this item is associated. + * If accompanied by a command, this data will be passed to the {@link MenuCommandExecutor}. + * If no command is present, this menu will be opened. */ - group: string; + menuPath?: MenuPath; + /** + * The path of the menu delegate that contributed this toolbar item + */ + delegateMenuPath?: MenuPath; + contextKeyOverlays?: Record; /** * Optional ordering string for placing the item within its group */ order?: string; } -/** - * Representation of an item in the tab - */ -export interface TabBarToolbarItem extends RegisteredToolbarItem, - ExecutableToolbarItem, - RenderedToolbarItem, - Omit, - Pick, - Partial, - Partial { } +export interface RenderedToolbarItem extends TabBarToolbarItemBase { + /** + * Optional icon for the item. + */ + icon?: string | (() => string); + + /** + * Optional text of the item. + * + * Strings in the format `$(iconIdentifier~animationType) will be treated as icon references. + * If the iconIdentifier begins with fa-, Font Awesome icons will be used; otherwise it will be treated as Codicon name. + * + * You can find Codicon classnames here: https://microsoft.github.io/vscode-codicons/dist/codicon.html + * You can find Font Awesome classnames here: http://fontawesome.io/icons/ + * The type of animation can be either `spin` or `pulse`. + */ + text?: string; + + /** + * Optional tooltip for the item. + */ + tooltip?: string; +} /** * Tab-bar toolbar item backed by a `React.ReactNode`. * Unlike the `TabBarToolbarItem`, this item is not connected to the command service. */ -export interface ReactTabBarToolbarItem extends RegisteredToolbarItem, - SelfRenderingToolbarItem, - ConditionalToolbarItem, - Pick, - Pick, 'group'> { } - -export interface AnyToolbarItem extends RegisteredToolbarItem, - Partial, - Partial, - Partial, - Partial, - Partial, - Pick, - Partial { } - -export interface MenuDelegate extends MenuToolbarItem, Required> { } +export interface ReactTabBarToolbarItem extends TabBarToolbarItemBase { + render(widget?: Widget): React.ReactNode; +} + +export namespace ReactTabBarToolbarItem { + export function is(item: TabBarToolbarItem): item is ReactTabBarToolbarItem { + return isObject(item) && typeof item.render === 'function'; + } +} + +export interface MenuDelegate { + menuPath: MenuPath; + isVisible(widget?: Widget): boolean; +} export namespace TabBarToolbarItem { @@ -160,48 +133,17 @@ export namespace TabBarToolbarItem { * Compares the items by `priority` in ascending. Undefined priorities will be treated as `0`. */ export const PRIORITY_COMPARATOR = (left: TabBarToolbarItem, right: TabBarToolbarItem) => { - const leftGroup = left.group ?? NAVIGATION; - const rightGroup = right.group ?? NAVIGATION; - if (leftGroup === NAVIGATION && rightGroup !== NAVIGATION) { return ArrayUtils.Sort.LeftBeforeRight; } - if (rightGroup === NAVIGATION && leftGroup !== NAVIGATION) { return ArrayUtils.Sort.RightBeforeLeft; } - if (leftGroup !== rightGroup) { return leftGroup.localeCompare(rightGroup); } + const leftGroup: string = left.group ?? NAVIGATION; + const rightGroup: string = right.group ?? NAVIGATION; + if (leftGroup === NAVIGATION && rightGroup !== NAVIGATION) { + return ArrayUtils.Sort.LeftBeforeRight; + } + if (rightGroup === NAVIGATION && leftGroup !== NAVIGATION) { + return ArrayUtils.Sort.RightBeforeLeft; + } + if (leftGroup !== rightGroup) { + return leftGroup.localeCompare(rightGroup); + } return (left.priority || 0) - (right.priority || 0); }; - - export function is(arg: unknown): arg is TabBarToolbarItem { - return isObject(arg) && isString(arg.command); - } - -} - -export namespace MenuToolbarItem { - /** - * Type guard for a toolbar item that actually is a menu item, amongst - * the other kinds of item that it may also be. - * - * @param item a toolbar item - * @returns whether the `item` is a menu item - */ - export function is(item: T): item is T & MenuToolbarItem { - return Array.isArray(item.menuPath); - } - - export function getMenuPath(item: AnyToolbarItem): MenuPath | undefined { - return Array.isArray(item.menuPath) ? item.menuPath : undefined; - } -} - -export namespace AnyToolbarItem { - /** - * Type guard for a toolbar item that actually manifests any of the - * features of a conditional toolbar item. - * - * @param item a toolbar item - * @returns whether the `item` is a conditional item - */ - export function isConditional(item: T): item is T & ConditionalToolbarItem { - return 'isVisible' in item && typeof item.isVisible === 'function' - || 'onDidChange' in item && typeof item.onDidChange === 'function' - || 'when' in item && typeof item.when === 'string'; - } } diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx index 092c5b90313fe..e5c65095477b0 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx @@ -16,13 +16,13 @@ import { inject, injectable, postConstruct } from 'inversify'; import * as React from 'react'; -import { ContextKeyService } from '../../context-key-service'; +import { ContextKeyService, ContextMatcher } from '../../context-key-service'; import { CommandRegistry, Disposable, DisposableCollection, MenuCommandExecutor, MenuModelRegistry, MenuPath, nls } from '../../../common'; import { Anchor, ContextMenuAccess, ContextMenuRenderer } from '../../context-menu-renderer'; import { LabelIcon, LabelParser } from '../../label-parser'; import { ACTION_ITEM, codicon, ReactWidget, Widget } from '../../widgets'; import { TabBarToolbarRegistry } from './tab-bar-toolbar-registry'; -import { AnyToolbarItem, ReactTabBarToolbarItem, TabBarDelegator, TabBarToolbarItem, TAB_BAR_TOOLBAR_CONTEXT_MENU, MenuToolbarItem } from './tab-bar-toolbar-types'; +import { ReactTabBarToolbarItem, TabBarDelegator, TabBarToolbarItem, TAB_BAR_TOOLBAR_CONTEXT_MENU, RenderedToolbarItem } from './tab-bar-toolbar-types'; import { KeybindingRegistry } from '../..//keybinding'; /** @@ -70,11 +70,11 @@ export class TabBarToolbar extends ReactWidget { @postConstruct() protected init(): void { - this.toDispose.push(this.keybindings.onKeybindingsChanged(() => this.update())); + this.toDispose.push(this.keybindings.onKeybindingsChanged(() => this.maybeUpdate())); this.toDispose.push(this.contextKeyService.onDidChange(e => { if (e.affects(this.keybindingContextKeys)) { - this.update(); + this.maybeUpdate(); } })); } @@ -87,10 +87,10 @@ export class TabBarToolbar extends ReactWidget { const contextKeys = new Set(); for (const item of items.sort(TabBarToolbarItem.PRIORITY_COMPARATOR).reverse()) { - if ('command' in item) { + if (item.command) { this.commands.getAllHandlers(item.command).forEach(handler => { if (handler.onDidChangeEnabled) { - this.toDisposeOnUpdateItems.push(handler.onDidChangeEnabled(() => this.update())); + this.toDisposeOnUpdateItems.push(handler.onDidChangeEnabled(() => this.maybeUpdate())); } }); } @@ -113,7 +113,7 @@ export class TabBarToolbar extends ReactWidget { } else { this.hide(); } - this.update(); + this.maybeUpdate(); } updateTarget(current?: Widget): void { @@ -130,7 +130,7 @@ export class TabBarToolbar extends ReactWidget { if (current) { const resetCurrent = () => { this.setCurrent(undefined); - this.update(); + this.maybeUpdate(); }; current.disposed.connect(resetCurrent); this.toDisposeOnSetCurrent.push(Disposable.create(() => @@ -144,7 +144,7 @@ export class TabBarToolbar extends ReactWidget { if (contextKeys.size > 0) { this.contextKeyListener = this.contextKeyService.onDidChange(event => { if (event.affects(contextKeys)) { - this.update(); + this.maybeUpdate(); } }); } @@ -154,9 +154,13 @@ export class TabBarToolbar extends ReactWidget { this.keybindingContextKeys.clear(); return {this.renderMore()} - {[...this.inline.values()].map(item => TabBarToolbarItem.is(item) - ? (MenuToolbarItem.is(item) && !item.command ? this.renderMenuItem(item) : this.renderItem(item)) - : item.render(this.current))} + {[...this.inline.values()].map(item => { + if (ReactTabBarToolbarItem.is(item)) { + return item.render(this.current); + } else { + return (item.menuPath && this.toolbarRegistry.isNonEmptyMenu(item, this.current) ? this.renderMenuItem(item) : this.renderItem(item)); + } + })} ; } @@ -180,7 +184,7 @@ export class TabBarToolbar extends ReactWidget { return result; } - protected renderItem(item: AnyToolbarItem): React.ReactNode { + protected renderItem(item: RenderedToolbarItem): React.ReactNode { let innerText = ''; const classNames = []; const command = item.command ? this.commands.getCommand(item.command) : undefined; @@ -222,13 +226,13 @@ export class TabBarToolbar extends ReactWidget { onMouseUp={this.onMouseUpEvent} onMouseOut={this.onMouseUpEvent} >
    this.executeCommand(e, item)} title={tooltip}>{innerText}
    ; } - protected isEnabled(item: AnyToolbarItem): boolean { + protected isEnabled(item: TabBarToolbarItem): boolean { if (!!item.command) { return this.commandIsEnabled(item.command) && this.evaluateWhenClause(item.when); } else { @@ -236,7 +240,7 @@ export class TabBarToolbar extends ReactWidget { } } - protected getToolbarItemClassNames(item: AnyToolbarItem): string[] { + protected getToolbarItemClassNames(item: TabBarToolbarItem): string[] { const classNames = [TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM]; if (item.command) { if (this.isEnabled(item)) { @@ -279,7 +283,7 @@ export class TabBarToolbar extends ReactWidget { if (subpath) { toDisposeOnHide.push(this.menus.linkSubmenu(TAB_BAR_TOOLBAR_CONTEXT_MENU, subpath)); } else { - for (const item of this.more.values() as IterableIterator) { + for (const item of this.more.values()) { if (item.menuPath && !item.command) { toDisposeOnHide.push(this.menus.linkSubmenu(TAB_BAR_TOOLBAR_CONTEXT_MENU, item.menuPath, undefined, item.group)); } else if (item.command) { @@ -293,10 +297,10 @@ export class TabBarToolbar extends ReactWidget { } } toDisposeOnHide.push(this.menus.registerMenuAction([...TAB_BAR_TOOLBAR_CONTEXT_MENU, ...item.group!.split('/')], { - label: item.tooltip, + label: (item as RenderedToolbarItem).tooltip, commandId: item.command, when: item.when, - order: item.order, + order: item.order })); } } @@ -318,14 +322,26 @@ export class TabBarToolbar extends ReactWidget { * @param item a toolbar item that is a menu item * @returns the rendered toolbar item */ - protected renderMenuItem(item: TabBarToolbarItem & MenuToolbarItem): React.ReactNode { - const icon = typeof item.icon === 'function' ? item.icon() : item.icon ?? 'ellipsis'; + protected renderMenuItem(item: RenderedToolbarItem): React.ReactNode { + const command = item.command ? this.commands.getCommand(item.command) : undefined; + const icon = (typeof item.icon === 'function' && item.icon()) || item.icon as string || (command && command.iconClass) || 'ellipsis'; + + let contextMatcher: ContextMatcher = this.contextKeyService; + if (item.contextKeyOverlays) { + contextMatcher = this.contextKeyService.createOverlay(Object.keys(item.contextKeyOverlays).map(key => [key, item.contextKeyOverlays![key]])); + } + return
    -
    -
    + className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM + ' enabled menu'} + > +
    this.executeCommand(e, item)} + /> +
    this.showPopupMenu(item.menuPath!, event, contextMatcher)}> +
    +
    +
    ; } @@ -336,11 +352,11 @@ export class TabBarToolbar extends ReactWidget { * @param menuPath the path of the registered menu to show * @param event the mouse event triggering the menu */ - protected showPopupMenu = (menuPath: MenuPath, event: React.MouseEvent) => { + protected showPopupMenu = (menuPath: MenuPath, event: React.MouseEvent, contextMatcher: ContextMatcher) => { event.stopPropagation(); event.preventDefault(); const anchor = this.toAnchor(event); - this.renderPopupMenu(menuPath, anchor); + this.renderPopupMenu(menuPath, anchor, contextMatcher); }; /** @@ -350,7 +366,7 @@ export class TabBarToolbar extends ReactWidget { * @param anchor a description of where to render the menu * @returns platform-specific access to the rendered context menu */ - protected renderPopupMenu(menuPath: MenuPath, anchor: Anchor): ContextMenuAccess { + protected renderPopupMenu(menuPath: MenuPath, anchor: Anchor, contextMatcher: ContextMatcher): ContextMenuAccess { const toDisposeOnHide = new DisposableCollection(); this.addClass('menu-open'); toDisposeOnHide.push(Disposable.create(() => this.removeClass('menu-open'))); @@ -360,6 +376,7 @@ export class TabBarToolbar extends ReactWidget { args: [this.current], anchor, context: this.current?.node, + contextKeyService: contextMatcher, onHide: () => toDisposeOnHide.dispose() }); } @@ -380,26 +397,30 @@ export class TabBarToolbar extends ReactWidget { return whenClause ? this.contextKeyService.match(whenClause, this.current?.node) : true; } - protected executeCommand = (e: React.MouseEvent) => { + protected executeCommand(e: React.MouseEvent, item: TabBarToolbarItem): void { e.preventDefault(); e.stopPropagation(); - const item: AnyToolbarItem | undefined = this.inline.get(e.currentTarget.id); - if (!item || !this.isEnabled(item)) { return; } - if (item.command && item.menuPath) { - this.menuCommandExecutor.executeCommand(item.menuPath, item.command, this.current); + if (item.command && item.delegateMenuPath) { + this.menuCommandExecutor.executeCommand(item.delegateMenuPath, item.command, this.current); } else if (item.command) { this.commands.executeCommand(item.command, this.current); } else if (item.menuPath) { this.renderMoreContextMenu(this.toAnchor(e), item.menuPath); } - this.update(); + this.maybeUpdate(); }; + protected maybeUpdate(): void { + if (!this.isDisposed) { + this.update(); + } + } + protected onMouseDownEvent = (e: React.MouseEvent) => { if (e.button === 0) { e.currentTarget.classList.add('active'); @@ -419,5 +440,4 @@ export namespace TabBarToolbar { export const TAB_BAR_TOOLBAR_ITEM = 'item'; } - } diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index c43f42c73bbcf..13980be53718f 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -17,7 +17,7 @@ import PerfectScrollbar from 'perfect-scrollbar'; import { TabBar, Title, Widget } from '@phosphor/widgets'; import { VirtualElement, h, VirtualDOM, ElementInlineStyle } from '@phosphor/virtualdom'; -import { Disposable, DisposableCollection, MenuPath, notEmpty, SelectionService, CommandService, nls } from '../../common'; +import { Disposable, DisposableCollection, MenuPath, notEmpty, SelectionService, CommandService, nls, ArrayUtils } from '../../common'; import { ContextMenuRenderer } from '../context-menu-renderer'; import { Signal, Slot } from '@phosphor/signaling'; import { Message, MessageLoop } from '@phosphor/messaging'; @@ -39,6 +39,7 @@ import { SelectComponent } from '../widgets/select-component'; import { createElement } from 'react'; import { PreviewableWidget } from '../widgets/previewable-widget'; import { EnhancedPreviewWidget } from '../widgets/enhanced-preview-widget'; +import { ContextKeyService } from '../context-key-service'; /** The class name added to hidden content nodes, which are required to render vertical side bars. */ const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content'; @@ -102,7 +103,8 @@ export class TabBarRenderer extends TabBar.Renderer { protected readonly selectionService?: SelectionService, protected readonly commandService?: CommandService, protected readonly corePreferences?: CorePreferences, - protected readonly hoverService?: HoverService + protected readonly hoverService?: HoverService, + protected readonly contextKeyService?: ContextKeyService, ) { super(); if (this.decoratorService) { @@ -170,8 +172,8 @@ export class TabBarRenderer extends TabBar.Renderer { const hover = this.tabBar && (this.tabBar.orientation === 'horizontal' && this.corePreferences?.['window.tabbar.enhancedPreview'] === 'classic') ? { title: title.caption } : { - onmouseenter: this.handleMouseEnterEvent - }; + onmouseenter: this.handleMouseEnterEvent + }; return h.li( { @@ -188,6 +190,7 @@ export class TabBarRenderer extends TabBar.Renderer { { className: 'theia-tab-icon-label' }, this.renderIcon(data, isInSidePanel), this.renderLabel(data, isInSidePanel), + this.renderTailDecorations(data, isInSidePanel), this.renderBadge(data, isInSidePanel), this.renderLock(data, isInSidePanel) ), @@ -289,6 +292,37 @@ export class TabBarRenderer extends TabBar.Renderer { return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label); } + protected renderTailDecorations(renderData: SideBarRenderData, isInSidePanel?: boolean): VirtualElement[] { + if (!this.corePreferences?.get('workbench.editor.decorations.badges')) { + return []; + } + const tailDecorations = ArrayUtils.coalesce(this.getDecorationData(renderData.title, 'tailDecorations')).flat(); + if (tailDecorations === undefined || tailDecorations.length === 0) { + return []; + } + let dotDecoration: WidgetDecoration.TailDecoration.AnyPartial | undefined; + const otherDecorations: WidgetDecoration.TailDecoration.AnyPartial[] = []; + tailDecorations.reverse().forEach(decoration => { + const partial = decoration as WidgetDecoration.TailDecoration.AnyPartial; + if (WidgetDecoration.TailDecoration.isDotDecoration(partial)) { + dotDecoration ||= partial; + } else if (partial.data || partial.icon || partial.iconClass) { + otherDecorations.push(partial); + } + }); + const decorationsToRender = dotDecoration ? [dotDecoration, ...otherDecorations] : otherDecorations; + return decorationsToRender.map((decoration, index) => { + const { tooltip, data, fontData, color, icon, iconClass } = decoration; + const iconToRender = icon ?? iconClass; + const className = ['p-TabBar-tail', 'flex'].join(' '); + const style = fontData ? fontData : color ? { color } : undefined; + const content = (data ? data : iconToRender + ? h.span({ className: this.getIconClass(iconToRender, iconToRender === 'circle' ? [WidgetDecoration.Styles.DECORATOR_SIZE_CLASS] : []) }) + : '') + (index !== decorationsToRender.length - 1 ? ',' : ''); + return h.span({ key: ('tailDecoration_' + index), className, style, title: tooltip ?? content }, content); + }); + } + renderBadge(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement { const totalBadge = this.getDecorationData(data.title, 'badge').reduce((sum, badge) => sum! + badge!, 0); if (!totalBadge) { @@ -585,7 +619,7 @@ export class TabBarRenderer extends TabBar.Renderer { cssClasses: ['extended-tab-preview'], visualPreview: this.corePreferences?.['window.tabbar.enhancedPreview'] === 'visual' ? width => this.renderVisualPreview(width, title) : undefined }); - } else { + } else if (title.caption) { this.hoverService.requestHover({ content: title.caption, target: event.currentTarget, @@ -614,10 +648,12 @@ export class TabBarRenderer extends TabBar.Renderer { this.selectionService.selection = NavigatableWidget.is(widget) ? { uri: widget.getResourceUri() } : widget; } + const contextKeyServiceOverlay = this.contextKeyService?.createOverlay([['isTerminalTab', widget && 'terminalId' in widget]]); this.contextMenuRenderer.render({ menuPath: this.contextMenuPath!, anchor: event, args: [event], + contextKeyService: contextKeyServiceOverlay, // We'd like to wait until the command triggered by the context menu has been run, but this should let it get through the preamble, at least. onHide: () => setTimeout(() => { if (this.selectionService) { this.selectionService.selection = oldSelection; } }) }); @@ -967,7 +1003,7 @@ export class ToolbarAwareTabBar extends ScrollableTabBar { protected override onBeforeDetach(msg: Message): void { if (this.toolbar && this.toolbar.isAttached) { - Widget.detach(this.toolbar); + this.toolbar.dispose(); } super.onBeforeDetach(msg); } @@ -1048,8 +1084,6 @@ export class SideTabBar extends ScrollableTabBar { startIndex: number }; - protected _rowGap: number; - constructor(options?: TabBar.IOptions & PerfectScrollbar.Options) { super(options); @@ -1106,31 +1140,6 @@ export class SideTabBar extends ScrollableTabBar { } } - // Queries the tabRowGap value of the content node. Needed to properly compute overflowing - // tabs that should be hidden - protected get tabRowGap(): number { - // We assume that the tab row gap is static i.e. we compute it once an then cache it - if (!this._rowGap) { - this._rowGap = this.computeTabRowGap(); - } - return this._rowGap; - - } - - protected computeTabRowGap(): number { - const style = window.getComputedStyle(this.contentNode); - const rowGapStyle = style.getPropertyValue('row-gap'); - const numericValue = parseFloat(rowGapStyle); - const unit = rowGapStyle.match(/[a-zA-Z]+/)?.[0]; - - const tempDiv = document.createElement('div'); - tempDiv.style.height = '1' + unit; - document.body.appendChild(tempDiv); - const rowGapValue = numericValue * tempDiv.offsetHeight; - document.body.removeChild(tempDiv); - return rowGapValue; - } - /** * Reveal the tab with the given index by moving it into the non-overflowing tabBar section * if necessary. @@ -1171,18 +1180,13 @@ export class SideTabBar extends ScrollableTabBar { const hiddenContent = this.hiddenContentNode; const n = hiddenContent.children.length; const renderData = new Array>(n); - const availableWidth = this.node.clientHeight - this.tabRowGap; - let actualWidth = 0; - let overflowStartIndex = -1; for (let i = 0; i < n; i++) { const hiddenTab = hiddenContent.children[i]; - // Extract tab padding from the computed style + // Extract tab padding, and margin from the computed style const tabStyle = window.getComputedStyle(hiddenTab); - const paddingTop = parseFloat(tabStyle.paddingTop!); - const paddingBottom = parseFloat(tabStyle.paddingBottom!); const rd: Partial = { - paddingTop, - paddingBottom + paddingTop: parseFloat(tabStyle.paddingTop!), + paddingBottom: parseFloat(tabStyle.paddingBottom!) }; // Extract label size from the DOM const labelElements = hiddenTab.getElementsByClassName('p-TabBar-tabLabel'); @@ -1195,38 +1199,21 @@ export class SideTabBar extends ScrollableTabBar { if (iconElements.length === 1) { const icon = iconElements[0]; rd.iconSize = { width: icon.clientWidth, height: icon.clientHeight }; - actualWidth += icon.clientHeight + paddingTop + paddingBottom + this.tabRowGap; - - if (actualWidth > availableWidth && i !== 0) { - rd.visible = false; - if (overflowStartIndex === -1) { - overflowStartIndex = i; - } - } - renderData[i] = rd; } - } - // Special handling if only one element is overflowing. - if (overflowStartIndex === n - 1 && renderData[overflowStartIndex]) { - if (!this.tabsOverflowData) { - overflowStartIndex--; - renderData[overflowStartIndex].visible = false; - } else { - renderData[overflowStartIndex].visible = true; - overflowStartIndex = -1; - } + renderData[i] = rd; } // Render into the visible node this.renderTabs(this.contentNode, renderData); - this.computeOverflowingTabsData(overflowStartIndex); + this.computeOverflowingTabsData(); }); } } - protected computeOverflowingTabsData(startIndex: number): void { + protected computeOverflowingTabsData(): void { // ensure that render tabs has completed window.requestAnimationFrame(() => { + const startIndex = this.hideOverflowingTabs(); if (startIndex === -1) { if (this.tabsOverflowData) { this.tabsOverflowData = undefined; @@ -1250,6 +1237,38 @@ export class SideTabBar extends ScrollableTabBar { }); } + /** + * Hide overflowing tabs and return the index of the first hidden tab. + */ + protected hideOverflowingTabs(): number { + const availableHeight = this.node.clientHeight; + const invisibleClass = 'p-mod-invisible'; + let startIndex = -1; + const n = this.contentNode.children.length; + for (let i = 0; i < n; i++) { + const tab = this.contentNode.children[i] as HTMLLIElement; + if (tab.offsetTop + tab.offsetHeight >= availableHeight) { + tab.classList.add(invisibleClass); + if (startIndex === -1) { + startIndex = i; + /* If only one element is overflowing and the additional menu widget is visible (i.e. this.tabsOverflowData is set) + * there might already be enough space to show the last tab. In this case, we need to include the size of the + * additional menu widget and recheck if the last tab is visible */ + if (startIndex === n - 1 && this.tabsOverflowData) { + const additionalViewsMenu = this.node.parentElement?.querySelector('.theia-additional-views-menu') as HTMLDivElement; + if (tab.offsetTop + tab.offsetHeight < availableHeight + additionalViewsMenu.offsetHeight) { + tab.classList.remove(invisibleClass); + startIndex = -1; + } + } + } + } else { + tab.classList.remove(invisibleClass); + } + } + return startIndex; + } + /** * Render the tab bar using the given DOM element as host. The optional `renderData` is forwarded * to the TabBarRenderer. diff --git a/packages/core/src/browser/shell/theia-dock-panel.ts b/packages/core/src/browser/shell/theia-dock-panel.ts index f5818217ec98f..13bc989e6ce91 100644 --- a/packages/core/src/browser/shell/theia-dock-panel.ts +++ b/packages/core/src/browser/shell/theia-dock-panel.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { find, toArray, ArrayExt } from '@phosphor/algorithm'; +import { find, toArray } from '@phosphor/algorithm'; import { TabBar, Widget, DockPanel, Title, DockLayout } from '@phosphor/widgets'; import { Signal } from '@phosphor/signaling'; import { Disposable, DisposableCollection } from '../../common/disposable'; @@ -103,7 +103,7 @@ export class TheiaDockPanel extends DockPanel { } findTabBar(title: Title): TabBar | undefined { - return find(this.tabBars(), bar => ArrayExt.firstIndexOf(bar.titles, title) > -1); + return find(this.tabBars(), bar => bar.titles.includes(title)); } protected readonly toDisposeOnMarkAsCurrent = new DisposableCollection(); @@ -133,11 +133,14 @@ export class TheiaDockPanel extends DockPanel { } } - override addWidget(widget: Widget, options?: DockPanel.IAddOptions): void { + override addWidget(widget: Widget, options?: TheiaDockPanel.AddOptions): void { if (this.mode === 'single-document' && widget.parent === this) { return; } super.addWidget(widget, options); + if (options?.closeRef) { + options.ref?.close(); + } this.widgetAdded.emit(widget); this.markActiveTabBar(widget.title); } @@ -252,4 +255,11 @@ export namespace TheiaDockPanel { export interface Factory { (options?: DockPanel.IOptions): TheiaDockPanel; } + + export interface AddOptions extends DockPanel.IAddOptions { + /** + * Whether to also close the widget referenced by `ref`. + */ + closeRef?: boolean + } } diff --git a/packages/plugin-ext/src/main/browser/view-column-service.ts b/packages/core/src/browser/shell/view-column-service.ts similarity index 92% rename from packages/plugin-ext/src/main/browser/view-column-service.ts rename to packages/core/src/browser/shell/view-column-service.ts index a44a18b6482f3..16db95bc190d8 100644 --- a/packages/plugin-ext/src/main/browser/view-column-service.ts +++ b/packages/core/src/browser/shell/view-column-service.ts @@ -14,11 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject } from '@theia/core/shared/inversify'; -import { Emitter, Event } from '@theia/core/lib/common/event'; -import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; -import { toArray } from '@theia/core/shared/@phosphor/algorithm'; -import { TabBar, Widget } from '@theia/core/shared/@phosphor/widgets'; +import { injectable, inject } from 'inversify'; +import { Emitter, Event } from '../../common/event'; +import { ApplicationShell } from './application-shell'; +import { toArray } from '@phosphor/algorithm'; +import { TabBar, Widget } from '@phosphor/widgets'; @injectable() export class ViewColumnService { diff --git a/packages/core/src/browser/shell/view-contribution.ts b/packages/core/src/browser/shell/view-contribution.ts index 7b5ba31579dac..bad02117f733d 100644 --- a/packages/core/src/browser/shell/view-contribution.ts +++ b/packages/core/src/browser/shell/view-contribution.ts @@ -18,7 +18,7 @@ import { injectable, inject, interfaces, optional } from 'inversify'; import { Widget } from '@phosphor/widgets'; import { MenuModelRegistry, Command, CommandContribution, - MenuContribution, CommandRegistry + MenuContribution, CommandRegistry, nls } from '../../common'; import { KeybindingContribution, KeybindingRegistry } from '../keybinding'; import { WidgetManager } from '../widget-manager'; @@ -69,7 +69,8 @@ export abstract class AbstractViewContribution implements Comm if (options.toggleCommandId) { this.toggleCommand = { id: options.toggleCommandId, - label: 'Toggle ' + this.viewLabel + ' View' + category: nls.localizeByDefault('View'), + label: nls.localizeByDefault('Toggle {0}', this.viewLabel) }; } } diff --git a/packages/core/src/browser/style/index.css b/packages/core/src/browser/style/index.css index bc3e99abe5ffe..3a69dc7c2c1c6 100644 --- a/packages/core/src/browser/style/index.css +++ b/packages/core/src/browser/style/index.css @@ -22,18 +22,18 @@ |----------------------------------------------------------------------------*/ :root { - /* Borders: Width and color (dark to bright) */ + /* Borders: Width and color (dark to bright) */ - --theia-border-width: 1px; - --theia-panel-border-width: 1px; + --theia-border-width: 1px; + --theia-panel-border-width: 1px; - /* UI fonts: Family, size and color (bright to dark) + /* UI fonts: Family, size and color (bright to dark) --------------------------------------------------- The UI font CSS variables are used for the typography all of the Theia user interface elements that are not directly user-generated content. */ - --theia-ui-font-scale-factor: 1.2; + --theia-ui-font-scale-factor: 1.2; --theia-ui-font-size0: calc( var(--theia-ui-font-size1) / var(--theia-ui-font-scale-factor) ); @@ -45,181 +45,186 @@ var(--theia-ui-font-size2) * var(--theia-ui-font-scale-factor) ); --theia-ui-icon-font-size: 14px; /* Ensures px perfect FontAwesome icons */ - --theia-ui-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + --theia-ui-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - /* Content fonts: Family, size and color (bright to dark) + /* Content fonts: Family, size and color (bright to dark) Content font variables are used for typography of user-generated content. */ - --theia-content-font-size: 13px; - --theia-content-line-height: 22px; + --theia-content-font-size: 13px; + --theia-content-line-height: 22px; - --theia-code-font-size: 13px; - --theia-code-line-height: 17px; - --theia-code-padding: 5px; - --theia-code-font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", - "Courier New", monospace, "Droid Sans Fallback"; - --theia-monospace-font-family: monospace; - --theia-ui-padding: 6px; + --theia-code-font-size: 13px; + --theia-code-line-height: 17px; + --theia-code-padding: 5px; + --theia-code-font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", + "Courier New", monospace, "Droid Sans Fallback"; + --theia-monospace-font-family: monospace; + --theia-ui-padding: 6px; - /* Icons */ - --theia-icon-size: 16px; + /* Icons */ + --theia-icon-size: 16px; - /* Scrollbars */ - --theia-scrollbar-width: 10px; - --theia-scrollbar-rail-width: 10px; + /* Scrollbars */ + --theia-scrollbar-width: 10px; + --theia-scrollbar-rail-width: 10px; - /* Statusbar */ - --theia-statusBar-font-size: 12px; + /* Statusbar */ + --theia-statusBar-font-size: 12px; - /* Opacity for disabled mod */ - --theia-mod-disabled-opacity: 0.4; + /* Opacity for disabled mod */ + --theia-mod-disabled-opacity: 0.4; } +html, body { - margin: 0; - padding: 0; - overflow: hidden; - font-family: var(--theia-ui-font-family); - background: var(--theia-editor-background); - color: var(--theia-foreground); - border: 1px solid var(--theia-window-activeBorder); + height: 100vh; +} + +body { + margin: 0; + padding: 0; + overflow: hidden; + font-family: var(--theia-ui-font-family); + background: var(--theia-editor-background); + color: var(--theia-foreground); + border: 1px solid var(--theia-window-activeBorder); } body:window-inactive, body:-moz-window-inactive { - border-color: var(--theia-window-inactiveBorder); + border-color: var(--theia-window-inactiveBorder); } a { - color: var(--theia-textLink-foreground); + color: var(--theia-textLink-foreground); } a:active, a:hover { - color: var(--theia-textLink-activeForeground); + color: var(--theia-textLink-activeForeground); } code { - color: var(--theia-textPreformat-foreground); + color: var(--theia-textPreformat-foreground); } blockquote { - margin: 0 7px 0 5px; - padding: 0px 16px 0px 10px; - background: var(--theia-textBlockQuote-background); - border-left: 5px solid var(--theia-textBlockQuote-border); + margin: 0 7px 0 5px; + padding: 0px 16px 0px 10px; + background: var(--theia-textBlockQuote-background); + border-left: 5px solid var(--theia-textBlockQuote-border); } .theia-input { - background: var(--theia-input-background); - color: var(--theia-input-foreground); - border: var(--theia-border-width) solid var(--theia-input-border); - font-family: var(--theia-ui-font-family); - font-size: var(--theia-ui-font-size1); - line-height: var(--theia-content-line-height); - padding-left: 5px; - border-radius: 2px; + background: var(--theia-input-background); + color: var(--theia-input-foreground); + border: var(--theia-border-width) solid var(--theia-input-border); + font-family: var(--theia-ui-font-family); + font-size: var(--theia-ui-font-size1); + line-height: var(--theia-content-line-height); + padding-left: 5px; + border-radius: 2px; } .theia-input[type="text"] { - text-overflow: ellipsis; - white-space: nowrap; + text-overflow: ellipsis; + white-space: nowrap; } .theia-input::placeholder { - color: var(--theia-input-placeholderForeground); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + color: var(--theia-input-placeholderForeground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .theia-maximized { - position: fixed !important; - top: 0 !important; - bottom: 0 !important; - left: 0 !important; - right: 0 !important; - width: auto !important; - height: auto !important; - z-index: 255 !important; - background: var(--theia-editor-background); + position: fixed !important; + top: 0 !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + width: auto !important; + height: auto !important; + z-index: 255 !important; + background: var(--theia-editor-background); } .theia-visible-menu-maximized { - top: var(--theia-private-menubar-height) !important; + top: var(--theia-private-menubar-height) !important; } .theia-ApplicationShell { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--theia-editor-background); + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--theia-editor-background); } .theia-preload { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 50000; - background: var(--theia-editor-background); - background-size: 60px 60px; - background-repeat: no-repeat; - background-attachment: fixed; - background-position: center; - transition: opacity 0.8s; - display: flex; - justify-content: center; - align-items: center; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 50000; + background: var(--theia-editor-background); + background-size: 60px 60px; + background-repeat: no-repeat; + background-attachment: fixed; + background-position: center; + transition: opacity 0.8s; + display: flex; + justify-content: center; + align-items: center; } .theia-preload::after { - animation: 1s theia-preload-rotate infinite; + animation: 1s theia-preload-rotate infinite; color: #777; /* color works on both light and dark themes */ content: "\eb19"; /* codicon-load */ - font: normal normal normal 72px/1 codicon; + font: normal normal normal 72px/1 codicon; } @keyframes theia-preload-rotate { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } .theia-preload.theia-hidden { - opacity: 0; + opacity: 0; } .theia-icon { - width: 32px; - height: 18px; - margin: 5px; - margin-left: 8px; + width: 32px; + height: 18px; + margin: 5px; + margin-left: 8px; } .theia-mod-disabled, .theia-mod-disabled:focus { - opacity: var(--theia-mod-disabled-opacity) !important; + opacity: var(--theia-mod-disabled-opacity) !important; } .theia-header { - text-transform: uppercase; - font-size: var(--theia-ui-font-size0); - font-weight: 700; + text-transform: uppercase; + font-size: var(--theia-ui-font-size0); + font-weight: 700; } .p-Widget { - font-size: var(--theia-ui-font-size1); + font-size: var(--theia-ui-font-size1); } .p-Widget.p-mod-hidden { - display: none !important; + display: none !important; } .noselect, @@ -231,91 +236,92 @@ blockquote { -ms-user-select: none; /* Internet Explorer/Edge */ user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ - -o-user-select: none; + -o-user-select: none; } -:focus { - outline-width: 1px; - outline-style: solid; - outline-offset: -1px; - opacity: 1; - outline-color: var(--theia-focusBorder); +/* Since an iframe has its own focus tracking, we don't show focus on iframes */ +:focus:not(iframe) { + outline-width: 1px; + outline-style: solid; + outline-offset: -1px; + opacity: 1; + outline-color: var(--theia-focusBorder); } ::selection { - background: var(--theia-selection-background); + background: var(--theia-selection-background); } .action-label { - padding: 2px; - border-radius: 5px; - cursor: pointer; + padding: 2px; + border-radius: 5px; + cursor: pointer; } .action-label:hover { - background-color: var(--theia-toolbar-hoverBackground); + background-color: var(--theia-toolbar-hoverBackground); } .theia-button { - border: none; - color: var(--theia-button-foreground); - min-width: 65px; - outline: none; - cursor: pointer; - padding: 4px 9px; - margin-left: calc(var(--theia-ui-padding) * 2); - border-radius: 2px; + border: none; + color: var(--theia-button-foreground); + min-width: 65px; + outline: none; + cursor: pointer; + padding: 4px 9px; + margin-left: calc(var(--theia-ui-padding) * 2); + border-radius: 2px; } .theia-button:focus { - outline: 1px solid var(--theia-focusBorder); - outline-offset: 1px; + outline: 1px solid var(--theia-focusBorder); + outline-offset: 1px; } .theia-button.secondary { - color: var(--theia-secondaryButton-foreground); + color: var(--theia-secondaryButton-foreground); } .theia-button[disabled] { - opacity: 0.6; - color: var(--theia-button-disabledForeground); - background-color: var(--theia-button-disabledBackground); - cursor: default; + opacity: 0.6; + color: var(--theia-button-disabledForeground); + background-color: var(--theia-button-disabledBackground); + cursor: default; } button.secondary[disabled], .theia-button.secondary[disabled] { - color: var(--theia-secondaryButton-disabledForeground); - background-color: var(--theia-secondaryButton-disabledBackground); + color: var(--theia-secondaryButton-disabledForeground); + background-color: var(--theia-secondaryButton-disabledBackground); } .theia-select { - color: var(--dropdown-foreground); - font-size: var(--theia-ui-font-size1); - border-radius: 2px; - border: 1px solid var(--theia-dropdown-border); - background: var(--theia-dropdown-background); - outline: none; - cursor: pointer; + color: var(--dropdown-foreground); + font-size: var(--theia-ui-font-size1); + border-radius: 2px; + border: 1px solid var(--theia-dropdown-border); + background: var(--theia-dropdown-background); + outline: none; + cursor: pointer; } .theia-select option { - background: var(--theia-dropdown-listBackground); + background: var(--theia-dropdown-listBackground); } .theia-transparent-overlay { - background-color: transparent; - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - z-index: 999; + background-color: transparent; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 999; } .theia-cursor-no-drop, .theia-cursor-no-drop:active { - cursor: no-drop; + cursor: no-drop; } /*----------------------------------------------------------------------------- @@ -344,3 +350,4 @@ button.secondary[disabled], @import "./progress-bar.css"; @import "./breadcrumbs.css"; @import "./tooltip.css"; +@import "./split-widget.css"; diff --git a/packages/core/src/browser/style/select-component.css b/packages/core/src/browser/style/select-component.css index fdcc1e175498f..887d9b7bb1a05 100644 --- a/packages/core/src/browser/style/select-component.css +++ b/packages/core/src/browser/style/select-component.css @@ -14,6 +14,13 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 ********************************************************************************/ +.theia-select-component-container { + /* required to set z-index */ + position: fixed; + /* dialog overlay has a z-index of 5000 */ + z-index: 6000; +} + .theia-select-component { background-color: var(--theia-dropdown-background); cursor: pointer; @@ -64,33 +71,25 @@ padding: 6px 5px; } -.theia-select-component-dropdown - .theia-select-component-description:first-child { +.theia-select-component-dropdown .theia-select-component-description:first-child { border-bottom: 1px solid var(--theia-editorWidget-border); margin-bottom: 2px; } -.theia-select-component-dropdown - .theia-select-component-description:last-child { +.theia-select-component-dropdown .theia-select-component-description:last-child { border-top: 1px solid var(--theia-editorWidget-border); margin-top: 2px; } -.theia-select-component-dropdown - .theia-select-component-option - .theia-select-component-option-value { +.theia-select-component-dropdown .theia-select-component-option .theia-select-component-option-value { width: 100%; } -.theia-select-component-dropdown - .theia-select-component-option - .theia-select-component-option-detail { +.theia-select-component-dropdown .theia-select-component-option .theia-select-component-option-detail { padding-left: 4px; } -.theia-select-component-dropdown - .theia-select-component-option:not(.selected) - .theia-select-component-option-detail { +.theia-select-component-dropdown .theia-select-component-option:not(.selected) .theia-select-component-option-detail { color: var(--theia-textLink-foreground); } diff --git a/packages/core/src/browser/style/sidepanel.css b/packages/core/src/browser/style/sidepanel.css index 374dd47d9e932..64d5a59532a3f 100644 --- a/packages/core/src/browser/style/sidepanel.css +++ b/packages/core/src/browser/style/sidepanel.css @@ -186,23 +186,26 @@ flex-direction: column-reverse; } +.p-Widget .theia-sidebar-menu-item { + cursor: pointer; +} + .p-Widget.theia-sidebar-menu i { padding: var(--theia-private-sidebar-tab-padding-top-and-bottom) var(--theia-private-sidebar-tab-padding-left-and-right); display: flex; justify-content: center; align-items: center; - cursor: pointer; color: var(--theia-activityBar-inactiveForeground); background-color: var(--theia-activityBar-background); font-size: var(--theia-private-sidebar-icon-size); } -.theia-sidebar-menu i:hover { +.theia-sidebar-menu .theia-sidebar-menu-item:hover i { color: var(--theia-activityBar-foreground); } -.theia-sidebar-menu > i.codicon-menu { +.theia-sidebar-menu i.theia-compact-menu { font-size: 16px; } diff --git a/packages/core/src/browser/style/split-widget.css b/packages/core/src/browser/style/split-widget.css new file mode 100644 index 0000000000000..2b18734fb2bea --- /dev/null +++ b/packages/core/src/browser/style/split-widget.css @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (C) 2024 1C-Soft LLC 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 + ********************************************************************************/ + +.theia-split-widget > .p-SplitPanel { + height: 100%; + width: 100%; + outline: none; +} + +.theia-split-widget > .p-SplitPanel > .p-SplitPanel-child { + min-width: 50px; + min-height: var(--theia-content-line-height); +} + +.theia-split-widget > .p-SplitPanel > .p-SplitPanel-handle { + box-sizing: border-box; +} + +.theia-split-widget > .p-SplitPanel[data-orientation="horizontal"] > .p-SplitPanel-handle { + border-left: var(--theia-border-width) solid var(--theia-sideBarSectionHeader-border); +} + +.theia-split-widget > .p-SplitPanel[data-orientation="vertical"] > .p-SplitPanel-handle { + border-top: var(--theia-border-width) solid var(--theia-sideBarSectionHeader-border); +} diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index ef247de6fcfcd..661030977fa90 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -9,10 +9,7 @@ --theia-private-horizontal-tab-scrollbar-height: 5px; --theia-tabbar-toolbar-z-index: 1001; --theia-toolbar-active-transform-scale: 1.272019649; - --theia-horizontal-toolbar-height: calc( - var(--theia-private-horizontal-tab-height) + - var(--theia-private-horizontal-tab-scrollbar-rail-height) / 2 - ); + --theia-horizontal-toolbar-height: calc(var(--theia-private-horizontal-tab-height) + var(--theia-private-horizontal-tab-scrollbar-rail-height) / 2); --theia-dragover-tab-border-width: 2px; } @@ -75,9 +72,7 @@ border-left: var(--theia-border-width) solid var(--theia-editorGroup-border); } -#theia-main-content-panel - .p-DockPanel-handle[data-orientation="vertical"] - + .p-TabBar { +#theia-main-content-panel .p-DockPanel-handle[data-orientation="vertical"]+.p-TabBar { border-top: var(--theia-border-width) solid var(--theia-editorGroup-border); } @@ -123,6 +118,12 @@ white-space: nowrap; } +.p-TabBar-tail { + padding-left: 5px; + text-align: center; + justify-content: center; +} + .p-TabBar.theia-app-centers .p-TabBar-tabLabelWrapper { display: flex; } @@ -136,11 +137,9 @@ -webkit-appearance: none; -moz-appearance: none; - background-image: linear-gradient( - 45deg, + background-image: linear-gradient(45deg, transparent 50%, - var(--theia-icon-foreground) 50% - ), + var(--theia-icon-foreground) 50%), linear-gradient(135deg, var(--theia-icon-foreground) 50%, transparent 50%); background-position: calc(100% - 6px) 8px, calc(100% - 2px) 8px, 100% 0; background-size: 4px 5px; @@ -171,6 +170,17 @@ padding-right: 8px; } +.p-TabBar.theia-app-centers .p-TabBar-tabIcon[class*="plugin-icon-"], +.p-TabBar-tab.p-mod-drag-image .p-TabBar-tabIcon[class*="plugin-icon-"] { + background: none; + height: var(--theia-icon-size); +} + +.p-TabBar.theia-app-centers .p-TabBar-tabIcon[class*="plugin-icon-"]::before, +.p-TabBar-tab.p-mod-drag-image .p-TabBar-tabIcon[class*="plugin-icon-"]::before { + display: inline-block; +} + /* codicons */ .p-TabBar.theia-app-centers .p-TabBar-tabIcon.codicon, .p-TabBar-tab.p-mod-drag-image .p-TabBar-tabIcon.codicon { @@ -208,12 +218,8 @@ visibility: hidden; } -.p-TabBar.theia-app-centers - .p-TabBar-tab.p-mod-closable - > .p-TabBar-tabCloseIcon, -.p-TabBar.theia-app-centers - .p-TabBar-tab.theia-mod-pinned - > .p-TabBar-tabCloseIcon { +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable>.p-TabBar-tabCloseIcon, +.p-TabBar.theia-app-centers .p-TabBar-tab.theia-mod-pinned>.p-TabBar-tabCloseIcon { padding: 2px; margin-top: 2px; margin-left: 4px; @@ -231,31 +237,19 @@ -ms-user-select: none; } -.p-TabBar.theia-app-centers.dynamic-tabs - .p-TabBar-tab.p-mod-closable - > .p-TabBar-tabCloseIcon, -.p-TabBar.theia-app-centers.dynamic-tabs - .p-TabBar-tab.theia-mod-pinned - > .p-TabBar-tabCloseIcon { +.p-TabBar.theia-app-centers.dynamic-tabs .p-TabBar-tab.p-mod-closable>.p-TabBar-tabCloseIcon, +.p-TabBar.theia-app-centers.dynamic-tabs .p-TabBar-tab.theia-mod-pinned>.p-TabBar-tabCloseIcon { /* hide close icon for dynamic tabs strategy*/ display: none; } -.p-TabBar.theia-app-centers - .p-TabBar-tab.p-mod-current - > .p-TabBar-tabCloseIcon, -.p-TabBar.theia-app-centers - .p-TabBar-tab:hover.p-mod-closable - > .p-TabBar-tabCloseIcon, -.p-TabBar.theia-app-centers - .p-TabBar-tab:hover.theia-mod-pinned - > .p-TabBar-tabCloseIcon { +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-current>.p-TabBar-tabCloseIcon, +.p-TabBar.theia-app-centers .p-TabBar-tab:hover.p-mod-closable>.p-TabBar-tabCloseIcon, +.p-TabBar.theia-app-centers .p-TabBar-tab:hover.theia-mod-pinned>.p-TabBar-tabCloseIcon { display: inline-block; } -.p-TabBar.theia-app-centers - .p-TabBar-tab.p-mod-closable - > .p-TabBar-tabCloseIcon:hover { +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable>.p-TabBar-tabCloseIcon:hover { border-radius: 5px; background-color: rgba(50%, 50%, 50%, 0.2); } @@ -265,33 +259,21 @@ padding-right: 4px; } -.p-TabBar.theia-app-centers - .p-TabBar-tab.p-mod-closable:not(.theia-mod-dirty):hover - > .p-TabBar-tabCloseIcon:before, -.p-TabBar.theia-app-centers - .p-TabBar-tab.p-mod-closable:not(.theia-mod-dirty).p-TabBar-tab.p-mod-current - > .p-TabBar-tabCloseIcon:before, -.p-TabBar.theia-app-centers - .p-TabBar-tab.p-mod-closable.theia-mod-dirty - > .p-TabBar-tabCloseIcon:hover:before { +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable:not(.theia-mod-dirty):hover>.p-TabBar-tabCloseIcon:before, +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable:not(.theia-mod-dirty).p-TabBar-tab.p-mod-current>.p-TabBar-tabCloseIcon:before, +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty>.p-TabBar-tabCloseIcon:hover:before { content: "\ea76"; } -.p-TabBar.theia-app-centers - .p-TabBar-tab.p-mod-closable.theia-mod-dirty - > .p-TabBar-tabCloseIcon:before { +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty>.p-TabBar-tabCloseIcon:before { content: "\ea71"; } -.p-TabBar.theia-app-centers - .p-TabBar-tab.theia-mod-pinned - > .p-TabBar-tabCloseIcon:before { +.p-TabBar.theia-app-centers .p-TabBar-tab.theia-mod-pinned>.p-TabBar-tabCloseIcon:before { content: "\eba0"; } -.p-TabBar.theia-app-centers - .p-TabBar-tab.theia-mod-pinned.theia-mod-dirty - > .p-TabBar-tabCloseIcon:before { +.p-TabBar.theia-app-centers .p-TabBar-tab.theia-mod-pinned.theia-mod-dirty>.p-TabBar-tabCloseIcon:before { content: "\ebb2"; } @@ -299,7 +281,7 @@ display: none !important; } -.p-TabBar .theia-badge-decorator-sidebar { +.theia-badge-decorator-sidebar { background-color: var(--theia-activityBarBadge-background); border-radius: 20px; color: var(--theia-activityBarBadge-foreground); @@ -334,72 +316,35 @@ | Perfect scrollbar |----------------------------------------------------------------------------*/ -.p-TabBar[data-orientation="horizontal"] - .p-TabBar-content-container - > .ps__rail-x { +.p-TabBar[data-orientation="horizontal"] .p-TabBar-content-container>.ps__rail-x { height: var(--theia-private-horizontal-tab-scrollbar-rail-height); z-index: 1000; } -.p-TabBar[data-orientation="horizontal"] - .p-TabBar-content-container - > .ps__rail-x - > .ps__thumb-x { +.p-TabBar[data-orientation="horizontal"] .p-TabBar-content-container>.ps__rail-x>.ps__thumb-x { height: var(--theia-private-horizontal-tab-scrollbar-height) !important; - bottom: calc( - ( - var(--theia-private-horizontal-tab-scrollbar-rail-height) - - var(--theia-private-horizontal-tab-scrollbar-height) - ) / 2 - ); -} - -.p-TabBar[data-orientation="horizontal"] - .p-TabBar-content-container - > .ps__rail-x:hover, -.p-TabBar[data-orientation="horizontal"] - .p-TabBar-content-container - > .ps__rail-x:focus { + bottom: calc((var(--theia-private-horizontal-tab-scrollbar-rail-height) - var(--theia-private-horizontal-tab-scrollbar-height)) / 2); +} + +.p-TabBar[data-orientation="horizontal"] .p-TabBar-content-container>.ps__rail-x:hover, +.p-TabBar[data-orientation="horizontal"] .p-TabBar-content-container>.ps__rail-x:focus { height: var(--theia-private-horizontal-tab-scrollbar-rail-height) !important; } -.p-TabBar[data-orientation="horizontal"] - .p-TabBar-content-container - > .ps__rail-x:hover - > .ps__thumb-x, -.p-TabBar[data-orientation="horizontal"] - .p-TabBar-content-container - > .ps__rail-x:focus - > .ps__thumb-x { - height: calc( - var(--theia-private-horizontal-tab-scrollbar-height) / 2 - ) !important; - bottom: calc( - ( - var(--theia-private-horizontal-tab-scrollbar-rail-height) - - var(--theia-private-horizontal-tab-scrollbar-height) - ) / 2 - ); -} - -.p-TabBar[data-orientation="vertical"] - .p-TabBar-content-container - > .ps__rail-y { +.p-TabBar[data-orientation="horizontal"] .p-TabBar-content-container>.ps__rail-x:hover>.ps__thumb-x, +.p-TabBar[data-orientation="horizontal"] .p-TabBar-content-container>.ps__rail-x:focus>.ps__thumb-x { + height: calc(var(--theia-private-horizontal-tab-scrollbar-height) / 2) !important; + bottom: calc((var(--theia-private-horizontal-tab-scrollbar-rail-height) - var(--theia-private-horizontal-tab-scrollbar-height)) / 2); +} + +.p-TabBar[data-orientation="vertical"] .p-TabBar-content-container>.ps__rail-y { width: var(--theia-private-horizontal-tab-scrollbar-rail-height); z-index: 1000; } -.p-TabBar[data-orientation="vertical"] - .p-TabBar-content-container - > .ps__rail-y - > .ps__thumb-y { +.p-TabBar[data-orientation="vertical"] .p-TabBar-content-container>.ps__rail-y>.ps__thumb-y { width: var(--theia-private-horizontal-tab-scrollbar-height) !important; - right: calc( - ( - var(--theia-private-horizontal-tab-scrollbar-rail-height) - - var(--theia-private-horizontal-tab-scrollbar-height) - ) / 2 - ); + right: calc((var(--theia-private-horizontal-tab-scrollbar-rail-height) - var(--theia-private-horizontal-tab-scrollbar-height)) / 2); } .p-TabBar[data-orientation="vertical"] .p-TabBar-content-container { @@ -430,9 +375,8 @@ |----------------------------------------------------------------------------*/ .p-TabBar-toolbar { - z-index: var( - --theia-tabbar-toolbar-z-index - ); /* Due to the scrollbar (`z-index: 1000;`) it has a greater `z-index`. */ + z-index: var(--theia-tabbar-toolbar-z-index); + /* Due to the scrollbar (`z-index: 1000;`) it has a greater `z-index`. */ display: flex; flex-direction: row-reverse; padding: 4px; @@ -443,15 +387,21 @@ .p-TabBar-content-container { display: flex; flex: 1; - position: relative; /* This is necessary for perfect-scrollbar */ + position: relative; + /* This is necessary for perfect-scrollbar */ } .p-TabBar-toolbar .item { - display: flex; - align-items: center; - margin-left: 4px; /* `padding` + `margin-right` from the container toolbar */ opacity: var(--theia-mod-disabled-opacity); cursor: default; + display: flex; + flex-direction: row; + column-gap: 0px; + align-items: centery; +} + +.p-TabBar-toolbar .item>div { + height: 100%; } .p-TabBar-toolbar .item.enabled { @@ -459,10 +409,6 @@ cursor: pointer; } -.p-TabBar-toolbar .item.enabled .action-label::before { - display: flex; -} - .p-TabBar-toolbar :not(.item.enabled) .action-label { background: transparent; cursor: default; @@ -473,14 +419,14 @@ background-color: var(--theia-inputOption-activeBackground); } -.p-TabBar-toolbar .item > div { - height: 18px; - width: 18px; +.p-TabBar-toolbar .item>div { + line-height: calc(var(--theia-icon-size) + 2px); + height: calc(var(--theia-icon-size) + 2px); background-repeat: no-repeat; line-height: 18px; } -.p-TabBar-toolbar .item > div.no-icon { +.p-TabBar-toolbar .item>div.no-icon { /* Make room for a text label instead of an icon. */ width: 100%; } @@ -505,30 +451,16 @@ background: var(--theia-icon-close) no-repeat; } -/** Configure layout of a toolbar item that shows a pop-up menu. */ -.p-TabBar-toolbar .item.menu { - display: grid; -} - -/** The elements of the item that shows a pop-up menu are stack atop one other. */ -.p-TabBar-toolbar .item.menu > div { - grid-area: 1 / 1; -} - /** * The chevron for the pop-up menu indication is shrunk and * stuffed in the bottom-right corner. */ -.p-TabBar-toolbar .item.menu > .chevron { - scale: 50%; - align-self: end; - justify-self: end; - translate: 5px 3px; +.p-TabBar-toolbar .item.menu .chevron { + font-size: 8px; + vertical-align: bottom; } -#theia-main-content-panel - .p-TabBar:not(.theia-tabBar-active) - .p-TabBar-toolbar { +#theia-main-content-panel .p-TabBar:not(.theia-tabBar-active) .p-TabBar-toolbar { display: none; } @@ -537,9 +469,7 @@ } .p-TabBar.theia-tabBar-multirow[data-orientation="horizontal"] { - min-height: calc( - var(--theia-breadcrumbs-height) + var(--theia-horizontal-toolbar-height) - ); + min-height: calc(var(--theia-breadcrumbs-height) + var(--theia-horizontal-toolbar-height)); flex-direction: column; } @@ -563,19 +493,14 @@ flex-direction: column; } -.p-TabBar.theia-app-centers[data-orientation="horizontal"].dynamic-tabs - .p-TabBar-tabLabel { +.p-TabBar.theia-app-centers[data-orientation="horizontal"].dynamic-tabs .p-TabBar-tabLabel { /* fade out text with dynamic tabs strategy */ - mask-image: linear-gradient( - to left, - rgba(0, 0, 0, 0.3), - rgba(0, 0, 0, 1) 15px - ); - -webkit-mask-image: linear-gradient( - to left, - rgba(0, 0, 0, 0.3), - rgba(0, 0, 0, 1) 15px - ); + mask-image: linear-gradient(to left, + rgba(0, 0, 0, 0.3), + rgba(0, 0, 0, 1) 15px); + -webkit-mask-image: linear-gradient(to left, + rgba(0, 0, 0, 0.3), + rgba(0, 0, 0, 1) 15px); flex: 1; } @@ -619,13 +544,11 @@ /*----------------------------------------------------------------------------- | Open tabs dropdown |----------------------------------------------------------------------------*/ -.theia-tabBar-open-tabs - > .theia-select-component - .theia-select-component-label { +.theia-tabBar-open-tabs>.theia-select-component .theia-select-component-label { display: none; } -.theia-tabBar-open-tabs > .theia-select-component { +.theia-tabBar-open-tabs>.theia-select-component { min-width: auto; height: 100%; } @@ -638,4 +561,4 @@ .theia-tabBar-open-tabs.p-mod-hidden { display: none; -} +} \ No newline at end of file diff --git a/packages/core/src/browser/style/view-container.css b/packages/core/src/browser/style/view-container.css index f3e6ce00bc5d2..59a9f9b5bd7e6 100644 --- a/packages/core/src/browser/style/view-container.css +++ b/packages/core/src/browser/style/view-container.css @@ -168,13 +168,6 @@ padding-right: calc(var(--theia-ui-padding) * 2 / 3); } -.theia-view-container-part-title .item > div { - height: var(--theia-icon-size); - width: var(--theia-icon-size); - background-size: var(--theia-icon-size); - line-height: var(--theia-icon-size); -} - .theia-view-container-part-title { display: none; } diff --git a/packages/core/src/browser/styling-service.ts b/packages/core/src/browser/styling-service.ts index 8acac1b1372be..221577711a09d 100644 --- a/packages/core/src/browser/styling-service.ts +++ b/packages/core/src/browser/styling-service.ts @@ -21,7 +21,7 @@ import { ColorRegistry } from './color-registry'; import { DecorationStyle } from './decoration-style'; import { FrontendApplicationContribution } from './frontend-application-contribution'; import { ThemeService } from './theming'; -import { Disposable } from '../common'; +import { SecondaryWindowHandler } from './secondary-window-handler'; export const StylingParticipant = Symbol('StylingParticipant'); @@ -52,16 +52,25 @@ export class StylingService implements FrontendApplicationContribution { @inject(ContributionProvider) @named(StylingParticipant) protected readonly themingParticipants: ContributionProvider; + @inject(SecondaryWindowHandler) + protected readonly secondaryWindowHandler: SecondaryWindowHandler; + onStart(): void { this.registerWindow(window); + this.secondaryWindowHandler.onWillAddWidget(([widget, window]) => { + this.registerWindow(window); + }); + this.secondaryWindowHandler.onWillRemoveWidget(([widget, window]) => { + this.cssElements.delete(window); + }); + this.themeService.onDidColorThemeChange(e => this.applyStylingToWindows(e.newTheme)); } - registerWindow(win: Window): Disposable { + registerWindow(win: Window): void { const cssElement = DecorationStyle.createStyleElement('contributedColorTheme', win.document.head); this.cssElements.set(win, cssElement); this.applyStyling(this.themeService.getCurrentTheme(), cssElement); - return Disposable.create(() => this.cssElements.delete(win)); } protected applyStylingToWindows(theme: Theme): void { diff --git a/packages/core/src/browser/test/mock-storage-service.ts b/packages/core/src/browser/test/mock-storage-service.ts index ecba9447fbf81..4fe74c224d79a 100644 --- a/packages/core/src/browser/test/mock-storage-service.ts +++ b/packages/core/src/browser/test/mock-storage-service.ts @@ -22,7 +22,7 @@ import { injectable } from 'inversify'; */ @injectable() export class MockStorageService implements StorageService { - readonly data = new Map(); + readonly data = new Map(); // eslint-disable-next-line @typescript-eslint/no-explicit-any onSetDataCallback?: (key: string, data?: any) => void; diff --git a/packages/core/src/browser/tooltip-service.tsx b/packages/core/src/browser/tooltip-service.tsx index 67fbc10285b56..0857960a434be 100644 --- a/packages/core/src/browser/tooltip-service.tsx +++ b/packages/core/src/browser/tooltip-service.tsx @@ -19,7 +19,7 @@ import * as React from 'react'; import ReactTooltip from 'react-tooltip'; import { ReactRenderer, RendererHost } from './widgets/react-renderer'; import { CorePreferences } from './core-preferences'; -import { v4 } from 'uuid'; +import { generateUuid } from '../common/uuid'; export const TooltipService = Symbol('TooltipService'); @@ -59,7 +59,7 @@ export class TooltipServiceImpl extends ReactRenderer implements TooltipService @inject(RendererHost) @optional() host?: RendererHost ) { super(host); - this.tooltipId = v4(); + this.tooltipId = generateUuid(); } @postConstruct() diff --git a/packages/core/src/browser/tree/index.ts b/packages/core/src/browser/tree/index.ts index 2361b0b62280f..03ce159fb9324 100644 --- a/packages/core/src/browser/tree/index.ts +++ b/packages/core/src/browser/tree/index.ts @@ -26,3 +26,4 @@ export * from './tree-container'; export * from './tree-decorator'; export * from './tree-search'; export * from './tree-compression'; +export * from './tree-preference'; diff --git a/packages/core/src/browser/tree/test/mock-selectable-tree-model.ts b/packages/core/src/browser/tree/test/mock-selectable-tree-model.ts new file mode 100644 index 0000000000000..7a6c263c14096 --- /dev/null +++ b/packages/core/src/browser/tree/test/mock-selectable-tree-model.ts @@ -0,0 +1,109 @@ +// ***************************************************************************** +// Copyright (C) 2023 Ericsson 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 { CompositeTreeNode } from '../tree'; +import { SelectableTreeNode } from '../tree-selection'; +import { ExpandableTreeNode } from '../tree-expansion'; + +export namespace MockSelectableTreeModel { + + export interface SelectableNode { + readonly id: string; + readonly selected: boolean; + readonly focused?: boolean; + readonly children?: SelectableNode[]; + } + + export namespace SelectableNode { + export function toTreeNode(root: SelectableNode, parent?: SelectableTreeNode & CompositeTreeNode): SelectableTreeNode { + const { id } = root; + const name = id; + const selected = false; + const focus = false; + const expanded = true; + const node: CompositeTreeNode & SelectableTreeNode = { + id, + name, + selected, + focus, + parent: parent, + children: [] + }; + const children = (root.children || []).map(child => SelectableNode.toTreeNode(child, node)); + if (children.length === 0) { + return node; + } else { + node.children = children; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (node as any).expanded = expanded; + return node as CompositeTreeNode & SelectableTreeNode & ExpandableTreeNode; + } + } + } + + export const HIERARCHICAL_MOCK_ROOT = () => SelectableNode.toTreeNode({ + 'id': '1', + 'selected': false, + 'children': [ + { + 'id': '1.1', + 'selected': false, + 'children': [ + { + 'id': '1.1.1', + 'selected': false, + }, + { + 'id': '1.1.2', + 'selected': false, + } + ] + }, + { + 'id': '1.2', + 'selected': false, + 'children': [ + { + 'id': '1.2.1', + 'selected': false, + 'children': [ + { + 'id': '1.2.1.1', + 'selected': false, + }, + { + 'id': '1.2.1.2', + 'selected': false, + } + ] + }, + { + 'id': '1.2.2', + 'selected': false, + }, + { + 'id': '1.2.3', + 'selected': false, + } + ] + }, + { + 'id': '1.3', + 'selected': false, + } + ] + }); +} diff --git a/packages/core/src/browser/tree/tree-compression/compressed-tree-widget.tsx b/packages/core/src/browser/tree/tree-compression/compressed-tree-widget.tsx index ee56f7bf3805b..b9502322dead5 100644 --- a/packages/core/src/browser/tree/tree-compression/compressed-tree-widget.tsx +++ b/packages/core/src/browser/tree/tree-compression/compressed-tree-widget.tsx @@ -58,6 +58,12 @@ export class CompressedTreeWidget extends TreeViewWelcomeWidget { } } + protected override shouldRenderIndent(node: TreeNode): boolean { + return !this.compressionToggle.compress + || !this.compressionService.isCompressionParticipant(node) + || this.compressionService.getCompressionHead(node) === node; + } + protected override shouldDisplayNode(node: TreeNode): boolean { if (this.compressionToggle.compress && this.compressionService.isCompressionParticipant(node) && !this.compressionService.isCompressionHead(node)) { return false; @@ -66,14 +72,18 @@ export class CompressedTreeWidget extends TreeViewWelcomeWidget { } protected override getDepthForNode(node: TreeNode, depths: Map): number { - if (!this.compressionToggle.compress) { return super.getDepthForNode(node, depths); } + if (!this.compressionToggle.compress) { + return super.getDepthForNode(node, depths); + } const parent = this.compressionService.getCompressionHead(node.parent) ?? node.parent; const parentDepth = depths.get(parent); return parentDepth === undefined ? 0 : TreeNode.isVisible(node.parent) ? parentDepth + 1 : parentDepth; } protected override toNodeRow(node: TreeNode, index: number, depth: number): CompressedNodeRow { - if (!this.compressionToggle.compress) { return super.toNodeRow(node, index, depth); } + if (!this.compressionToggle.compress) { + return super.toNodeRow(node, index, depth); + } const row: CompressedNodeRow = { node, index, depth }; if (this.compressionService.isCompressionHead(node)) { row.compressionChain = this.compressionService.getCompressionChain(node); @@ -102,7 +112,9 @@ export class CompressedTreeWidget extends TreeViewWelcomeWidget { } protected override getCaptionChildren(node: TreeNode, props: CompressedNodeProps): React.ReactNode { - if (!this.compressionToggle.compress || !props.compressionChain) { return super.getCaptionChildren(node, props); } + if (!this.compressionToggle.compress || !props.compressionChain) { + return super.getCaptionChildren(node, props); + } return props.compressionChain.map((subNode, index, self) => { const classes = ['theia-tree-compressed-label-part']; if (SelectableTreeNode.isSelected(subNode)) { @@ -129,21 +141,27 @@ export class CompressedTreeWidget extends TreeViewWelcomeWidget { } protected override handleUp(event: KeyboardEvent): void { - if (!this.compressionToggle.compress) { return super.handleUp(event); } + if (!this.compressionToggle.compress) { + return super.handleUp(event); + } const type = this.props.multiSelect && this.hasShiftMask(event) ? TreeSelection.SelectionType.RANGE : undefined; this.model.selectPrevRow(type); this.node.focus(); } protected override handleDown(event: KeyboardEvent): void { - if (!this.compressionToggle.compress) { return super.handleDown(event); } + if (!this.compressionToggle.compress) { + return super.handleDown(event); + } const type = this.props.multiSelect && this.hasShiftMask(event) ? TreeSelection.SelectionType.RANGE : undefined; this.model.selectNextRow(type); this.node.focus(); } protected override async handleLeft(event: KeyboardEvent): Promise { - if (!this.compressionToggle.compress) { return super.handleLeft(event); } + if (!this.compressionToggle.compress) { + return super.handleLeft(event); + } if (Boolean(this.props.multiSelect) && (this.hasCtrlCmdMask(event) || this.hasShiftMask(event))) { return; } @@ -160,7 +178,9 @@ export class CompressedTreeWidget extends TreeViewWelcomeWidget { } protected override async handleRight(event: KeyboardEvent): Promise { - if (!this.compressionToggle.compress) { return super.handleRight(event); } + if (!this.compressionToggle.compress) { + return super.handleRight(event); + } if (Boolean(this.props.multiSelect) && (this.hasCtrlCmdMask(event) || this.hasShiftMask(event))) { return; } diff --git a/packages/core/src/browser/tree/tree-model.ts b/packages/core/src/browser/tree/tree-model.ts index 97526a3da4c0d..d4a6dda0c167c 100644 --- a/packages/core/src/browser/tree/tree-model.ts +++ b/packages/core/src/browser/tree/tree-model.ts @@ -99,21 +99,41 @@ export interface TreeModel extends Tree, TreeSelectionService, TreeExpansionServ */ navigateBackward(): Promise; + /** + * Selects the previous tree node, regardless of its selection or visibility state. + */ + selectPrev(): void; + /** * Selects the previous node relatively to the currently selected one. This method takes the expansion state of the tree into consideration. */ selectPrevNode(type?: TreeSelection.SelectionType): void; + /** + * Returns the previous tree node, regardless of its selection or visibility state. + */ + getPrevNode(node?: TreeNode): TreeNode | undefined; + /** * Returns the previous selectable tree node. */ getPrevSelectableNode(node?: TreeNode): SelectableTreeNode | undefined; + /** + * Selects the next tree node, regardless of its selection or visibility state. + */ + selectNext(): void; + /** * Selects the next node relatively to the currently selected one. This method takes the expansion state of the tree into consideration. */ selectNextNode(type?: TreeSelection.SelectionType): void; + /** + * Returns the next tree node, regardless of its selection or visibility state. + */ + getNextNode(node?: TreeNode): TreeNode | undefined; + /** * Returns the next selectable tree node. */ @@ -294,6 +314,11 @@ export class TreeModelImpl implements TreeModel, SelectionProvider(iterator: TreeIterator, criterion: (node: TreeNode) => node is T): T | undefined { // Skip the first item. // TODO: clean this up, and skip the first item in a different way without loading everything. iterator.next(); @@ -338,6 +390,17 @@ export class TreeModelImpl implements TreeModel, SelectionProvider; + +export function bindTreePreferences(bind: interfaces.Bind): void { + bind(TreePreferences).toDynamicValue(ctx => { + const factory = ctx.container.get(PreferenceProxyFactory); + return factory(treePreferencesSchema); + }).inSingletonScope(); + bind(PreferenceContribution).toConstantValue({ schema: treePreferencesSchema }); +} diff --git a/packages/core/src/browser/tree/tree-selectable.spec.ts b/packages/core/src/browser/tree/tree-selectable.spec.ts new file mode 100644 index 0000000000000..b8cc16d22a8a7 --- /dev/null +++ b/packages/core/src/browser/tree/tree-selectable.spec.ts @@ -0,0 +1,152 @@ +// ***************************************************************************** +// Copyright (C) 2018 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 { TreeNode } from './tree'; +import { TreeModel } from './tree-model'; +import { notEmpty } from '../../common/objects'; +import { expect } from 'chai'; +import { createTreeTestContainer } from './test/tree-test-container'; +import { SelectableTreeNode } from './tree-selection'; +import { MockSelectableTreeModel } from './test/mock-selectable-tree-model'; +import { ExpandableTreeNode } from './tree-expansion'; + +describe('Selectable Tree', () => { + let model: TreeModel; + function assertNodeRetrieval(method: () => TreeNode | undefined, sequence: string[]): void { + for (const expectedNodeId of sequence) { + const actualNode = method(); + const expectedNode = retrieveNode(expectedNodeId); + expect(actualNode?.id).to.be.equal(expectedNode.id); + model.addSelection(expectedNode); + } + } + function assertNodeSelection(method: () => void, sequence: string[]): void { + for (const expectedNodeId of sequence) { + method(); + const node = retrieveNode(expectedNodeId); + expect(node.selected).to.be.true; + } + } + describe('Get and Set Next Nodes Methods', () => { + const uncollapsedSelectionOrder = ['1.1', '1.1.1', '1.1.2', '1.2', '1.2.1', '1.2.1.1', '1.2.1.2', '1.2.2', '1.2.3', '1.3']; + const collapsedSelectionOrder = ['1.1', '1.2', '1.2.1', '1.2.2', '1.2.3', '1.3']; + beforeEach(() => { + model = createTreeModel(); + model.root = MockSelectableTreeModel.HIERARCHICAL_MOCK_ROOT(); + model.addSelection(retrieveNode('1')); + + }); + it('`getNextNode()` should select each node in sequence (uncollapsed)', done => { + assertNodeRetrieval(model.getNextNode.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`getNextNode()` should select each node in sequence (collapsed)', done => { + collapseNode('1.1', '1.2.1'); + assertNodeRetrieval(model.getNextNode.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`getNextSelectableNode()` should select each node in sequence (uncollapsed)', done => { + assertNodeRetrieval(model.getNextSelectableNode.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`getNextSelectableNode()` should select each node in sequence (collapsed)', done => { + collapseNode('1.1', '1.2.1'); + assertNodeRetrieval(model.getNextSelectableNode.bind(model), collapsedSelectionOrder); + done(); + }); + it('`selectNext()` should select each node in sequence (uncollapsed)', done => { + assertNodeSelection(model.selectNext.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`selectNext()` should select each node in sequence (collapsed)', done => { + collapseNode('1.1', '1.2.1'); + assertNodeSelection(model.selectNext.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`selectNextNode()` should select each node in sequence (uncollapsed)', done => { + assertNodeSelection(model.selectNextNode.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`selectNextNode()` should select each node in sequence (collapsed)', done => { + collapseNode('1.1', '1.2.1'); + assertNodeSelection(model.selectNextNode.bind(model), collapsedSelectionOrder); + done(); + }); + }); + + describe('Get and Set Previous Nodes Methods', () => { + const uncollapsedSelectionOrder = ['1.2.3', '1.2.2', '1.2.1.2', '1.2.1.1', '1.2.1', '1.2', '1.1.2', '1.1.1', '1.1']; + const collapsedSelectionOrder = ['1.2.3', '1.2.2', '1.2.1', '1.2', '1.1']; + beforeEach(() => { + model = createTreeModel(); + model.root = MockSelectableTreeModel.HIERARCHICAL_MOCK_ROOT(); + model.addSelection(retrieveNode('1.3')); + }); + it('`getPrevNode()` should select each node in reverse sequence (uncollapsed)', done => { + assertNodeRetrieval(model.getPrevNode.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`getPrevNode()` should select each node in reverse sequence (collapsed)', done => { + collapseNode('1.1', '1.2.1'); + assertNodeRetrieval(model.getPrevNode.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`getPrevSelectableNode()` should select each node in reverse sequence (uncollapsed)', done => { + assertNodeRetrieval(model.getPrevSelectableNode.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`getPrevSelectableNode()` should select each node in reverse sequence (collapsed)', done => { + collapseNode('1.1', '1.2.1'); + assertNodeRetrieval(model.getPrevSelectableNode.bind(model), collapsedSelectionOrder); + done(); + }); + it('`selectPrev()` should select each node in reverse sequence (uncollapsed)', done => { + assertNodeSelection(model.selectPrev.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`selectPrev()` should select each node in reverse sequence (collapsed)', done => { + collapseNode('1.1', '1.2.1'); + assertNodeSelection(model.selectPrev.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`selectPrevNode()` should select each node in reverse sequence (uncollapsed)', done => { + assertNodeSelection(model.selectPrevNode.bind(model), uncollapsedSelectionOrder); + done(); + }); + it('`selectPrevNode()` should select each node in reverse sequence (collapsed)', done => { + collapseNode('1.1', '1.2.1'); + assertNodeSelection(model.selectPrevNode.bind(model), collapsedSelectionOrder); + done(); + }); + }); + + const findNode = (id: string) => model.getNode(id); + function createTreeModel(): TreeModel { + const container = createTreeTestContainer(); + return container.get(TreeModel); + } + function retrieveNode(id: string): Readonly { + const readonlyNode: Readonly = model.getNode(id) as T; + return readonlyNode; + } + function collapseNode(...ids: string[]): void { + ids.map(findNode).filter(notEmpty).filter(ExpandableTreeNode.is).forEach(node => { + model.collapseNode(node); + expect(node).to.have.property('expanded', false); + }); + } + +}); diff --git a/packages/core/src/browser/tree/tree-widget.tsx b/packages/core/src/browser/tree/tree-widget.tsx index d2034c3fd0421..9888aae99874b 100644 --- a/packages/core/src/browser/tree/tree-widget.tsx +++ b/packages/core/src/browser/tree/tree-widget.tsx @@ -43,6 +43,8 @@ import { LabelProvider } from '../label-provider'; import { CorePreferences } from '../core-preferences'; import { TreeFocusService } from './tree-focus-service'; import { useEffect } from 'react'; +import { PreferenceService, PreferenceChange } from '../preferences'; +import { PREFERENCE_NAME_TREE_INDENT } from './tree-preference'; const debounce = require('lodash.debounce'); @@ -73,8 +75,7 @@ export interface TreeProps { readonly contextMenuPath?: MenuPath; /** - * The size of the padding (in pixels) per hierarchy depth. The root element won't have left padding but - * the padding for the children will be calculated as `leftPadding * hierarchyDepth` and so on. + * The size of the padding (in pixels) for the root node of the tree. */ readonly leftPadding: number; @@ -174,6 +175,9 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { @inject(SelectionService) protected readonly selectionService: SelectionService; + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @@ -182,6 +186,8 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { protected shouldScrollToRow = true; + protected treeIndent: number = 8; + constructor( @inject(TreeProps) readonly props: TreeProps, @inject(TreeModel) readonly model: TreeModel, @@ -198,6 +204,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { @postConstruct() protected init(): void { + this.treeIndent = this.preferenceService.get(PREFERENCE_NAME_TREE_INDENT, this.treeIndent); if (this.props.search) { this.searchBox = this.searchBoxFactory({ ...SearchBoxProps.DEFAULT, showButtons: true, showFilter: true }); this.searchBox.node.addEventListener('focus', () => { @@ -264,6 +271,12 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { return; } } + }), + this.preferenceService.onPreferenceChanged((event: PreferenceChange) => { + if (event.preferenceName === PREFERENCE_NAME_TREE_INDENT) { + this.treeIndent = event.newValue; + this.update(); + } }) ]); setTimeout(() => { @@ -289,6 +302,12 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { } }) ]); + + this.node.addEventListener('focusin', e => { + if (this.model.selectedNodes.length && (!this.selectionService.selection || !TreeWidgetSelection.isSource(this.selectionService.selection, this))) { + this.updateGlobalSelection(); + } + }); } this.toDispose.push(this.corePreferences.onPreferenceChanged(preference => { if (preference.preferenceName === 'workbench.tree.renderIndentGuides') { @@ -326,7 +345,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { } } this.rows = new Map(rowsToUpdate); - this.updateScrollToRow(); + this.update(); } protected getDepthForNode(node: TreeNode, depths: Map): number { @@ -894,22 +913,33 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { let current: TreeNode | undefined = node; let depth = props.depth; while (current && depth) { - const classNames: string[] = [TREE_NODE_INDENT_GUIDE_CLASS]; - if (this.needsActiveIndentGuideline(current)) { - classNames.push('active'); - } else { - classNames.push(renderIndentGuides === 'onHover' ? 'hover' : 'always'); + if (this.shouldRenderIndent(current)) { + const classNames: string[] = [TREE_NODE_INDENT_GUIDE_CLASS]; + if (this.needsActiveIndentGuideline(current)) { + classNames.push('active'); + } else { + classNames.push(renderIndentGuides === 'onHover' ? 'hover' : 'always'); + } + const paddingLeft = this.getDepthPadding(depth); + indentDivs.unshift(
    ); + depth--; } - const paddingLeft = this.getDepthPadding(depth); - indentDivs.unshift(
    ); current = current.parent; - depth--; } return indentDivs; } + /** + * Determines whether an indentation div should be rendered for the specified tree node. + * If there are multiple tree nodes inside of a single rendered row, + * this method should only return true for the first node. + */ + protected shouldRenderIndent(node: TreeNode): boolean { + return true; + } + protected needsActiveIndentGuideline(node: TreeNode): boolean { const parent = node.parent; if (!parent || !this.isExpandable(parent)) { @@ -1216,12 +1246,15 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { /** * Handle the `space key` keyboard event. - * - By default should be similar to a single-click action. + * - If the element has a checkbox, it will be toggled. + * - Otherwise, it should be similar to a single-click action. * @param event the `space key` keyboard event. */ protected handleSpace(event: KeyboardEvent): void { const { focusedNode } = this.focusService; - if (!this.props.multiSelect || (!event.ctrlKey && !event.metaKey && !event.shiftKey)) { + if (focusedNode && focusedNode.checkboxInfo) { + this.model.markAsChecked(focusedNode, !focusedNode.checkboxInfo.checked); + } else if (!this.props.multiSelect || (!event.ctrlKey && !event.metaKey && !event.shiftKey)) { this.tapNode(focusedNode); } } @@ -1486,7 +1519,10 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { return this.labelProvider.getLongName(node); } protected getDepthPadding(depth: number): number { - return depth * this.props.leftPadding; + if (depth === 1) { + return this.props.leftPadding; + } + return depth * this.treeIndent; } } export namespace TreeWidget { diff --git a/packages/core/src/browser/tree/tree.ts b/packages/core/src/browser/tree/tree.ts index 3e78cab724d47..432b67dac15f0 100644 --- a/packages/core/src/browser/tree/tree.ts +++ b/packages/core/src/browser/tree/tree.ts @@ -397,9 +397,10 @@ export class TreeImpl implements Tree { protected async doMarkAsBusy(node: Mutable, ms: number, token: CancellationToken): Promise { try { + token.onCancellationRequested(() => this.doResetBusy(node)); await timeout(ms, token); + if (token.isCancellationRequested) { return; } this.doSetBusy(node); - token.onCancellationRequested(() => this.doResetBusy(node)); } catch { /* no-op */ } diff --git a/packages/core/src/browser/undo-redo-handler.ts b/packages/core/src/browser/undo-redo-handler.ts new file mode 100644 index 0000000000000..180ab9098d678 --- /dev/null +++ b/packages/core/src/browser/undo-redo-handler.ts @@ -0,0 +1,85 @@ +// ***************************************************************************** +// 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 { inject, injectable, named, postConstruct } from 'inversify'; +import { ContributionProvider } from '../common'; + +export const UndoRedoHandler = Symbol('UndoRedoHandler'); + +export interface UndoRedoHandler { + priority: number; + select(): T | undefined; + undo(item: T): void; + redo(item: T): void; +} + +@injectable() +export class UndoRedoHandlerService { + + @inject(ContributionProvider) @named(UndoRedoHandler) + protected readonly provider: ContributionProvider>; + + protected handlers: UndoRedoHandler[]; + + @postConstruct() + protected init(): void { + this.handlers = this.provider.getContributions().sort((a, b) => b.priority - a.priority); + } + + undo(): void { + for (const handler of this.handlers) { + const selection = handler.select(); + if (selection) { + handler.undo(selection); + return; + } + } + } + + redo(): void { + for (const handler of this.handlers) { + const selection = handler.select(); + if (selection) { + handler.redo(selection); + return; + } + } + } + +} + +@injectable() +export class DomInputUndoRedoHandler implements UndoRedoHandler { + + priority = 1000; + + select(): Element | undefined { + const element = document.activeElement; + if (element && ['input', 'textarea'].includes(element.tagName.toLowerCase())) { + return element; + } + return undefined; + } + + undo(item: Element): void { + document.execCommand('undo'); + } + + redo(item: Element): void { + document.execCommand('redo'); + } + +} diff --git a/packages/core/src/browser/user-working-directory-provider.ts b/packages/core/src/browser/user-working-directory-provider.ts index 002ed8e4bf475..ccc9c63a14bc5 100644 --- a/packages/core/src/browser/user-working-directory-provider.ts +++ b/packages/core/src/browser/user-working-directory-provider.ts @@ -16,14 +16,34 @@ import { inject, injectable } from 'inversify'; import URI from '../common/uri'; -import { MaybePromise, SelectionService, UriSelection } from '../common'; +import { MaybePromise, SelectionService, UNTITLED_SCHEME, UriSelection } from '../common'; import { EnvVariablesServer } from '../common/env-variables'; +import { FrontendApplication } from './frontend-application'; +import { FrontendApplicationContribution } from './frontend-application-contribution'; +import { Widget } from './widgets'; +import { Navigatable } from './navigatable-types'; @injectable() -export class UserWorkingDirectoryProvider { +export class UserWorkingDirectoryProvider implements FrontendApplicationContribution { @inject(SelectionService) protected readonly selectionService: SelectionService; @inject(EnvVariablesServer) protected readonly envVariables: EnvVariablesServer; + protected lastOpenResource: URI | undefined; + + configure(app: FrontendApplication): void { + app.shell.onDidChangeCurrentWidget(e => this.setLastOpenResource(e.newValue ?? undefined)); + this.setLastOpenResource(app.shell.currentWidget); + } + + protected setLastOpenResource(widget?: Widget): void { + if (Navigatable.is(widget)) { + const uri = widget.getResourceUri(); + if (uri && uri.scheme !== UNTITLED_SCHEME) { + this.lastOpenResource = uri; + } + } + } + /** * @returns A {@link URI} that represents a good guess about the directory in which the user is currently operating. * @@ -35,7 +55,16 @@ export class UserWorkingDirectoryProvider { } protected getFromSelection(): MaybePromise { - return this.ensureIsDirectory(UriSelection.getUri(this.selectionService.selection)); + const uri = UriSelection.getUri(this.selectionService.selection); + if (uri?.scheme === UNTITLED_SCHEME) { + // An untitled file is not a valid working directory context. + return undefined; + } + return this.ensureIsDirectory(uri); + } + + protected getFromLastOpenResource(): MaybePromise { + return this.ensureIsDirectory(this.lastOpenResource); } protected getFromUserHome(): MaybePromise { diff --git a/packages/core/src/browser/view-container.ts b/packages/core/src/browser/view-container.ts index b45fe54169901..ef39d86d12980 100644 --- a/packages/core/src/browser/view-container.ts +++ b/packages/core/src/browser/view-container.ts @@ -29,7 +29,7 @@ import { MAIN_AREA_ID, BOTTOM_AREA_ID } from './shell/theia-dock-panel'; import { FrontendApplicationStateService } from './frontend-application-state'; import { ContextMenuRenderer, Anchor } from './context-menu-renderer'; import { parseCssMagnitude } from './browser'; -import { TabBarToolbarRegistry, TabBarToolbarFactory, TabBarToolbar, TabBarDelegator, TabBarToolbarItem } from './shell/tab-bar-toolbar'; +import { TabBarToolbarRegistry, TabBarToolbarFactory, TabBarToolbar, TabBarDelegator, RenderedToolbarItem } from './shell/tab-bar-toolbar'; import { isEmpty, isObject, nls } from '../common'; import { WidgetManager } from './widget-manager'; import { Key } from './keys'; @@ -324,7 +324,7 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica return 'view'; } - protected registerToolbarItem(commandId: string, options?: Partial>): void { + protected registerToolbarItem(commandId: string, options?: Partial>): void { const newId = `${this.id}-tabbar-toolbar-${commandId}`; const existingHandler = this.commandRegistry.getAllHandlers(commandId)[0]; const existingCommand = this.commandRegistry.getCommand(commandId); diff --git a/packages/core/src/browser/widget-manager.ts b/packages/core/src/browser/widget-manager.ts index 56c1ba61e2dbb..960131b2c1379 100644 --- a/packages/core/src/browser/widget-manager.ts +++ b/packages/core/src/browser/widget-manager.ts @@ -114,7 +114,7 @@ export class WidgetManager { protected _cachedFactories: Map; protected readonly widgets = new Map(); - protected readonly pendingWidgetPromises = new Map>(); + protected readonly pendingWidgetPromises = new Map>(); @inject(ContributionProvider) @named(WidgetFactory) protected readonly factoryProvider: ContributionProvider; @@ -194,6 +194,31 @@ export class WidgetManager { return widget; } + /** + * Finds a widget that matches the given test predicate. + * @param factoryId The widget factory id. + * @param predicate The test predicate. + * + * @returns a promise resolving to the widget if available, else `undefined`. + */ + async findWidget(factoryId: string, predicate: (options?: any) => boolean): Promise { + for (const [key, widget] of this.widgets.entries()) { + if (this.testPredicate(key, factoryId, predicate)) { + return widget as T; + } + } + for (const [key, widgetPromise] of this.pendingWidgetPromises.entries()) { + if (this.testPredicate(key, factoryId, predicate)) { + return widgetPromise as Promise; + } + } + } + + protected testPredicate(key: string, factoryId: string, predicate: (options?: any) => boolean): boolean { + const constructionOptions = this.fromKey(key); + return constructionOptions.factoryId === factoryId && predicate(constructionOptions.options); + } + protected doGetWidget(key: string): MaybePromise | undefined { const pendingWidget = this.widgets.get(key) ?? this.pendingWidgetPromises.get(key); if (pendingWidget) { @@ -219,18 +244,26 @@ export class WidgetManager { if (!factory) { throw Error("No widget factory '" + factoryId + "' has been registered."); } - try { - const widgetPromise = factory.createWidget(options); - this.pendingWidgetPromises.set(key, widgetPromise); - const widget = await widgetPromise; - await WaitUntilEvent.fire(this.onWillCreateWidgetEmitter, { factoryId, widget }); + const widgetPromise = this.doCreateWidget(factory, options).then(widget => { this.widgets.set(key, widget); widget.disposed.connect(() => this.widgets.delete(key)); this.onDidCreateWidgetEmitter.fire({ factoryId, widget }); - return widget as T; - } finally { - this.pendingWidgetPromises.delete(key); + return widget; + }).finally(() => this.pendingWidgetPromises.delete(key)); + this.pendingWidgetPromises.set(key, widgetPromise); + return widgetPromise; + } + + protected async doCreateWidget(factory: WidgetFactory, options?: any): Promise { + const widget = await factory.createWidget(options); + // Note: the widget creation process also includes the 'onWillCreateWidget' part, which can potentially fail + try { + await WaitUntilEvent.fire(this.onWillCreateWidgetEmitter, { factoryId: factory.id, widget }); + } catch (e) { + widget.dispose(); + throw e; } + return widget as T; } /** diff --git a/packages/core/src/browser/widget-open-handler.ts b/packages/core/src/browser/widget-open-handler.ts index 7c08ca1d02845..25802c6359869 100644 --- a/packages/core/src/browser/widget-open-handler.ts +++ b/packages/core/src/browser/widget-open-handler.ts @@ -24,7 +24,10 @@ import { WidgetManager } from './widget-manager'; export type WidgetOpenMode = 'open' | 'reveal' | 'activate'; /** - * `WidgetOpenerOptions` define serializable generic options used by the {@link WidgetOpenHandler}. + * `WidgetOpenerOptions` define generic options used by the {@link WidgetOpenHandler}. + * + * _Note:_ This object may contain references to widgets (e.g. `widgetOptions.ref`); + * these need to be transformed before it can be serialized. */ export interface WidgetOpenerOptions extends OpenerOptions { /** @@ -96,7 +99,7 @@ export abstract class WidgetOpenHandler implements OpenHan ...options }; if (!widget.isAttached) { - this.shell.addWidget(widget, op.widgetOptions || { area: 'main' }); + await this.shell.addWidget(widget, op.widgetOptions || { area: 'main' }); } if (op.mode === 'activate') { await this.shell.activateWidget(widget.id); @@ -143,12 +146,12 @@ export abstract class WidgetOpenHandler implements OpenHan protected getWidget(uri: URI, options?: WidgetOpenerOptions): Promise { const widgetOptions = this.createWidgetOptions(uri, options); - return this.widgetManager.getWidget(this.id, widgetOptions); + return this.widgetManager.getWidget(this.id, widgetOptions); } protected getOrCreateWidget(uri: URI, options?: WidgetOpenerOptions): Promise { const widgetOptions = this.createWidgetOptions(uri, options); - return this.widgetManager.getOrCreateWidget(this.id, widgetOptions); + return this.widgetManager.getOrCreateWidget(this.id, widgetOptions); } protected abstract createWidgetOptions(uri: URI, options?: WidgetOpenerOptions): Object; diff --git a/packages/core/src/browser/widget-status-bar-service.ts b/packages/core/src/browser/widget-status-bar-service.ts new file mode 100644 index 0000000000000..d705880d90546 --- /dev/null +++ b/packages/core/src/browser/widget-status-bar-service.ts @@ -0,0 +1,84 @@ +// ***************************************************************************** +// 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 { inject, injectable, named } from 'inversify'; +import { Widget } from './widgets'; +import { StatusBar } from './status-bar'; +import { FrontendApplicationContribution } from './frontend-application-contribution'; +import { ContributionProvider } from '../common'; +import { FrontendApplication } from './frontend-application'; + +export const WidgetStatusBarContribution = Symbol('WidgetStatusBarContribution'); + +export interface WidgetStatusBarContribution { + canHandle(widget: Widget): widget is T; + activate(statusBar: StatusBar, widget: T): void; + deactivate(statusBar: StatusBar): void; +} + +/** + * Creates an empty {@link WidgetStatusBarContribution} that does nothing. + * Useful for widgets that are not handled by any other contribution, for example: + * * Settings widget + * * Welcome widget + * * Webview widget + * + * @param prototype Prototype to identify the kind of the widget. + * @returns An empty {@link WidgetStatusBarContribution}. + */ +export function noopWidgetStatusBarContribution(prototype: Function): WidgetStatusBarContribution { + return { + canHandle(widget: Widget): widget is Widget { + return widget instanceof prototype; + }, + activate: () => { }, + deactivate: () => { } + }; +} + +@injectable() +export class WidgetStatusBarService implements FrontendApplicationContribution { + + @inject(ContributionProvider) @named(WidgetStatusBarContribution) + protected readonly contributionProvider: ContributionProvider>; + + @inject(StatusBar) + protected readonly statusBar: StatusBar; + + onStart(app: FrontendApplication): void { + app.shell.onDidChangeCurrentWidget(event => { + if (event.newValue) { + this.show(event.newValue); + } + }); + } + + protected show(widget: Widget): void { + const contributions = this.contributionProvider.getContributions(); + // If any contribution can handle the widget, activate it + // If none can, keep everything as is + if (contributions.some(contribution => contribution.canHandle(widget))) { + for (const contribution of contributions) { + // Deactivate all contributions + contribution.deactivate(this.statusBar); + if (contribution.canHandle(widget)) { + // Selectively re-activate them + contribution.activate(this.statusBar, widget); + } + } + } + } +} diff --git a/packages/core/src/browser/widgets/extractable-widget.ts b/packages/core/src/browser/widgets/extractable-widget.ts index d5ae144348edb..640bfab6656a4 100644 --- a/packages/core/src/browser/widgets/extractable-widget.ts +++ b/packages/core/src/browser/widgets/extractable-widget.ts @@ -28,6 +28,6 @@ export interface ExtractableWidget extends Widget { export namespace ExtractableWidget { export function is(widget: unknown): widget is ExtractableWidget { - return widget instanceof Widget && widget.hasOwnProperty('isExtractable') && (widget as ExtractableWidget).isExtractable === true; + return widget instanceof Widget && 'isExtractable' in widget && (widget as ExtractableWidget).isExtractable === true; } } diff --git a/packages/core/src/browser/widgets/index.ts b/packages/core/src/browser/widgets/index.ts index a8539dea88602..48ef9cf5ca42e 100644 --- a/packages/core/src/browser/widgets/index.ts +++ b/packages/core/src/browser/widgets/index.ts @@ -18,3 +18,4 @@ export * from './widget'; export * from './react-renderer'; export * from './react-widget'; export * from './extractable-widget'; +export * from './split-widget'; diff --git a/packages/core/src/browser/widgets/react-renderer.tsx b/packages/core/src/browser/widgets/react-renderer.tsx index f24325072ac13..ff8176405e2d2 100644 --- a/packages/core/src/browser/widgets/react-renderer.tsx +++ b/packages/core/src/browser/widgets/react-renderer.tsx @@ -41,7 +41,10 @@ export class ReactRenderer implements Disposable { } render(): void { - this.hostRoot.render({this.doRender()}); + // Ignore all render calls after the host element has unmounted + if (!this.toDispose.disposed) { + this.hostRoot.render({this.doRender()}); + } } protected doRender(): React.ReactNode { diff --git a/packages/core/src/browser/widgets/react-widget.tsx b/packages/core/src/browser/widgets/react-widget.tsx index b1b300be35588..38fe93ce26f6b 100644 --- a/packages/core/src/browser/widgets/react-widget.tsx +++ b/packages/core/src/browser/widgets/react-widget.tsx @@ -38,7 +38,9 @@ export abstract class ReactWidget extends BaseWidget { protected override onUpdateRequest(msg: Message): void { super.onUpdateRequest(msg); - this.nodeRoot.render({this.render()}); + if (!this.isDisposed) { + this.nodeRoot.render({this.render()}); + } } /** diff --git a/packages/core/src/browser/widgets/select-component.tsx b/packages/core/src/browser/widgets/select-component.tsx index 68bcdd22d325a..0e038e5a8043e 100644 --- a/packages/core/src/browser/widgets/select-component.tsx +++ b/packages/core/src/browser/widgets/select-component.tsx @@ -77,6 +77,7 @@ export class SelectComponent extends React.Component(); + readonly onDidChangeTrackableWidgets = this.onDidChangeTrackableWidgetsEmitter.event; + + protected readonly compositeSaveable = new CompositeSaveable(); + + protected navigatable?: Navigatable; + + constructor(options?: SplitPanel.IOptions & { navigatable?: Navigatable }) { + super(); + + this.toDispose.pushAll([this.onDidChangeTrackableWidgetsEmitter]); + + this.addClass('theia-split-widget'); + + const layout = new PanelLayout(); + this.layout = layout; + const that = this; + this.splitPanel = new class extends SplitPanel { + + protected override onChildAdded(msg: Widget.ChildMessage): void { + super.onChildAdded(msg); + that.onPaneAdded(msg.child); + } + + protected override onChildRemoved(msg: Widget.ChildMessage): void { + super.onChildRemoved(msg); + that.onPaneRemoved(msg.child); + } + }({ + spacing: 1, // --theia-border-width + ...options + }); + this.splitPanel.node.tabIndex = -1; + layout.addWidget(this.splitPanel); + + this.navigatable = options?.navigatable; + } + + get orientation(): SplitPanel.Orientation { + return this.splitPanel.orientation; + } + + set orientation(value: SplitPanel.Orientation) { + this.splitPanel.orientation = value; + } + + relativeSizes(): number[] { + return this.splitPanel.relativeSizes(); + } + + setRelativeSizes(sizes: number[]): void { + this.splitPanel.setRelativeSizes(sizes); + } + + get handles(): readonly HTMLDivElement[] { + return this.splitPanel.handles; + } + + get saveable(): Saveable { + return this.compositeSaveable; + } + + getResourceUri(): URI | undefined { + return this.navigatable?.getResourceUri(); + } + + createMoveToUri(resourceUri: URI): URI | undefined { + return this.navigatable?.createMoveToUri(resourceUri); + } + + storeState(): SplitWidget.State { + return { orientation: this.orientation, widgets: this.panes, relativeSizes: this.relativeSizes() }; + } + + restoreState(oldState: SplitWidget.State): void { + const { orientation, widgets, relativeSizes } = oldState; + if (orientation) { + this.orientation = orientation; + } + for (const widget of widgets) { + this.addPane(widget); + } + if (relativeSizes) { + this.setRelativeSizes(relativeSizes); + } + } + + get panes(): readonly Widget[] { + return this.splitPanel.widgets; + } + + getTrackableWidgets(): Widget[] { + return [...this.panes]; + } + + protected fireDidChangeTrackableWidgets(): void { + this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets()); + } + + addPane(pane: Widget): void { + this.splitPanel.addWidget(pane); + } + + insertPane(index: number, pane: Widget): void { + this.splitPanel.insertWidget(index, pane); + } + + protected onPaneAdded(pane: Widget): void { + if (Saveable.isSource(pane)) { + this.compositeSaveable.add(pane.saveable); + } + this.fireDidChangeTrackableWidgets(); + } + + protected onPaneRemoved(pane: Widget): void { + if (Saveable.isSource(pane)) { + this.compositeSaveable.remove(pane.saveable); + } + this.fireDidChangeTrackableWidgets(); + } + + protected override onActivateRequest(msg: Message): void { + this.splitPanel.node.focus(); + } +} + +export namespace SplitWidget { + export interface State { + orientation?: SplitPanel.Orientation; + widgets: readonly Widget[]; // note: don't rename this property; it has special meaning for `ShellLayoutRestorer` + relativeSizes?: number[]; + } +} diff --git a/packages/core/src/browser/widgets/widget.ts b/packages/core/src/browser/widgets/widget.ts index 8ea9b5959e141..9a71b96074208 100644 --- a/packages/core/src/browser/widgets/widget.ts +++ b/packages/core/src/browser/widgets/widget.ts @@ -381,12 +381,20 @@ export function pin(title: Title): void { } } +export function isLocked(title: Title): boolean { + return title.className.includes(LOCKED_CLASS); +} + export function lock(title: Title): void { if (!title.className.includes(LOCKED_CLASS)) { title.className += ` ${LOCKED_CLASS}`; } } +export function unlock(title: Title): void { + title.className = title.className.replace(LOCKED_CLASS, '').trim(); +} + export function togglePinned(title?: Title): void { if (title) { if (isPinned(title)) { diff --git a/packages/core/src/browser/window/default-secondary-window-service.ts b/packages/core/src/browser/window/default-secondary-window-service.ts index 7dc9745f52b34..4e415476f5887 100644 --- a/packages/core/src/browser/window/default-secondary-window-service.ts +++ b/packages/core/src/browser/window/default-secondary-window-service.ts @@ -19,6 +19,9 @@ import { WindowService } from './window-service'; import { ExtractableWidget } from '../widgets'; import { ApplicationShell } from '../shell'; import { Saveable } from '../saveable'; +import { PreferenceService } from '../preferences'; +import { environment } from '../../common'; +import { SaveableService } from '../saveable-service'; @injectable() export class DefaultSecondaryWindowService implements SecondaryWindowService { @@ -38,6 +41,12 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService { @inject(WindowService) protected readonly windowService: WindowService; + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + @inject(SaveableService) + protected readonly saveResourceService: SaveableService; + @postConstruct() init(): void { // Set up messaging with secondary windows @@ -77,35 +86,18 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService { } createSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): Window | undefined { - const win = this.doCreateSecondaryWindow(widget, shell); - if (win) { - this.secondaryWindows.push(win); - win.addEventListener('close', () => { - const extIndex = this.secondaryWindows.indexOf(win); - if (extIndex > -1) { - this.secondaryWindows.splice(extIndex, 1); - }; - }); + const [height, width, left, top] = this.findSecondaryWindowCoordinates(widget); + let options = `popup=1,width=${width},height=${height},left=${left},top=${top}`; + if (this.preferenceService.get('window.secondaryWindowAlwaysOnTop')) { + options += ',alwaysOnTop=true'; } - return win; - } - - protected findWindow(windowName: string): Window | undefined { - for (const w of this.secondaryWindows) { - if (w.name === windowName) { - return w; - } - } - return undefined; - } - - protected doCreateSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): Window | undefined { - const newWindow = window.open(DefaultSecondaryWindowService.SECONDARY_WINDOW_URL, this.nextWindowId(), 'popup') ?? undefined; + const newWindow = window.open(DefaultSecondaryWindowService.SECONDARY_WINDOW_URL, this.nextWindowId(), options) ?? undefined; if (newWindow) { + this.secondaryWindows.push(newWindow); newWindow.addEventListener('DOMContentLoaded', () => { newWindow.addEventListener('beforeunload', evt => { const saveable = Saveable.get(widget); - const wouldLoseState = !!saveable && saveable.dirty && saveable.autoSave === 'off'; + const wouldLoseState = !!saveable && saveable.dirty && this.saveResourceService.autoSave === 'off'; if (wouldLoseState) { evt.returnValue = ''; evt.preventDefault(); @@ -113,17 +105,80 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService { } }, { capture: true }); - newWindow.addEventListener('close', () => { + newWindow.addEventListener('unload', () => { const saveable = Saveable.get(widget); shell.closeWidget(widget.id, { - save: !!saveable && saveable.dirty && saveable.autoSave !== 'off' + save: !!saveable && saveable.dirty && this.saveResourceService.autoSave !== 'off' }); + + const extIndex = this.secondaryWindows.indexOf(newWindow); + if (extIndex > -1) { + this.secondaryWindows.splice(extIndex, 1); + }; }); + this.windowCreated(newWindow, widget, shell); }); } return newWindow; } + protected windowCreated(newWindow: Window, widget: ExtractableWidget, shell: ApplicationShell): void { + newWindow.addEventListener('unload', () => { + shell.closeWidget(widget.id); + }); + } + + protected findWindow(windowName: string): Window | undefined { + for (const w of this.secondaryWindows) { + if (w.name === windowName) { + return w; + } + } + return undefined; + } + + protected findSecondaryWindowCoordinates(widget: ExtractableWidget): (number | undefined)[] { + const clientBounds = widget.node.getBoundingClientRect(); + const preference = this.preferenceService.get('window.secondaryWindowPlacement'); + + let height; let width; let left; let top; + const offsetY = 20; // Offset to avoid the window title bar + + switch (preference) { + case 'originalSize': { + height = widget.node.clientHeight; + width = widget.node.clientWidth; + left = window.screenLeft + clientBounds.x; + top = window.screenTop + (window.outerHeight - window.innerHeight) + offsetY; + if (environment.electron.is()) { + top = window.screenTop + clientBounds.y; + } + break; + } + case 'halfWidth': { + height = window.innerHeight - (window.outerHeight - window.innerHeight); + width = window.innerWidth / 2; + left = window.screenLeft; + top = window.screenTop; + if (!environment.electron.is()) { + height = window.innerHeight + clientBounds.y - offsetY; + } + break; + } + case 'fullSize': { + height = window.innerHeight - (window.outerHeight - window.innerHeight); + width = window.innerWidth; + left = window.screenLeft; + top = window.screenTop; + if (!environment.electron.is()) { + height = window.innerHeight + clientBounds.y - offsetY; + } + break; + } + } + return [height, width, left, top]; + } + focus(win: Window): void { win.focus(); } diff --git a/packages/core/src/browser/window/default-window-service.ts b/packages/core/src/browser/window/default-window-service.ts index bee19fa4a0408..f2d83ca5858fb 100644 --- a/packages/core/src/browser/window/default-window-service.ts +++ b/packages/core/src/browser/window/default-window-service.ts @@ -57,6 +57,10 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC this.openNewWindow(`#${DEFAULT_WINDOW_HASH}`); } + focus(): void { + window.focus(); + } + /** * Returns a list of actions that {@link FrontendApplicationContribution}s would like to take before shutdown * It is expected that this will succeed - i.e. return an empty array - at most once per session. If no vetoes are received diff --git a/packages/core/src/browser/window/test/mock-window-service.ts b/packages/core/src/browser/window/test/mock-window-service.ts index 3d924337b04a5..245c0c691acfd 100644 --- a/packages/core/src/browser/window/test/mock-window-service.ts +++ b/packages/core/src/browser/window/test/mock-window-service.ts @@ -21,6 +21,7 @@ import { WindowService } from '../window-service'; export class MockWindowService implements WindowService { openNewWindow(): undefined { return undefined; } openNewDefaultWindow(): void { } + focus(): void { } reload(): void { } isSafeToShutDown(): Promise { return Promise.resolve(true); } setSafeToShutDown(): void { } diff --git a/packages/core/src/browser/window/window-service.ts b/packages/core/src/browser/window/window-service.ts index 34adb43737c22..6f1a0fc7bb40e 100644 --- a/packages/core/src/browser/window/window-service.ts +++ b/packages/core/src/browser/window/window-service.ts @@ -18,6 +18,11 @@ import { StopReason } from '../../common/frontend-application-state'; import { Event } from '../../common/event'; import { NewWindowOptions, WindowSearchParams } from '../../common/window'; +export interface WindowReloadOptions { + search?: WindowSearchParams, + hash?: string +} + /** * Service for opening new browser windows. */ @@ -35,7 +40,12 @@ export interface WindowService { * Opens a new default window. * - In electron and in the browser it will open the default window without a pre-defined content. */ - openNewDefaultWindow(params?: WindowSearchParams): void; + openNewDefaultWindow(params?: WindowReloadOptions): void; + + /** + * Reveal and focuses the current window + */ + focus(): void; /** * Fires when the `window` unloads. The unload event is inevitable. On this event, the frontend application can save its state and release resource. @@ -64,5 +74,5 @@ export interface WindowService { /** * Reloads the window according to platform. */ - reload(params?: WindowSearchParams): void; + reload(params?: WindowReloadOptions): void; } diff --git a/packages/core/src/common/application-protocol.ts b/packages/core/src/common/application-protocol.ts index d84822c908d0a..a96361a71bb05 100644 --- a/packages/core/src/common/application-protocol.ts +++ b/packages/core/src/common/application-protocol.ts @@ -23,6 +23,8 @@ export const ApplicationServer = Symbol('ApplicationServer'); export interface ApplicationServer { getExtensionsInfos(): Promise; getApplicationInfo(): Promise; + getApplicationRoot(): Promise; + getApplicationPlatform(): Promise; /** * @deprecated since 1.25.0. Use `OS.backend.type()` instead. */ diff --git a/packages/core/src/common/command.ts b/packages/core/src/common/command.ts index d0e3f6302095a..0e32824c8b20f 100644 --- a/packages/core/src/common/command.ts +++ b/packages/core/src/common/command.ts @@ -41,6 +41,10 @@ export interface Command { * An icon class of this command. */ iconClass?: string; + /** + * A short title used for display in menus. + */ + shortTitle?: string; /** * A category of this command. */ diff --git a/packages/core/src/common/disposable.spec.ts b/packages/core/src/common/disposable.spec.ts index 0149c7adc27c4..e80d83bd1c294 100644 --- a/packages/core/src/common/disposable.spec.ts +++ b/packages/core/src/common/disposable.spec.ts @@ -14,8 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from 'chai'; +import { expect, spy, use } from 'chai'; import { DisposableCollection, Disposable } from './disposable'; +import * as spies from 'chai-spies'; + +use(spies); describe('Disposables', () => { it('Is safe to use Disposable.NULL', () => { @@ -26,4 +29,66 @@ describe('Disposables', () => { expect(collectionA.disposed, 'A should be disposed after being disposed.').to.be.true; expect(collectionB.disposed, 'B should not be disposed because A was disposed.').to.be.false; }); + + it('Collection is auto-pruned when an element is disposed', () => { + const onDispose = spy(() => { }); + const elementDispose = () => { }; + + const collection = new DisposableCollection(); + collection.onDispose(onDispose); + + const disposable1 = Disposable.create(elementDispose); + collection.push(disposable1); + expect(collection['disposables']).to.have.lengthOf(1); + + const disposable2 = Disposable.create(elementDispose); + collection.push(disposable2); + expect(collection['disposables']).to.have.lengthOf(2); + + disposable1.dispose(); + expect(collection['disposables']).to.have.lengthOf(1); + expect(onDispose).to.have.not.been.called(); + expect(collection.disposed).is.false; + + // Test that calling dispose on an already disposed element doesn't + // alter the collection state + disposable1.dispose(); + expect(collection['disposables']).to.have.lengthOf(1); + expect(onDispose).to.have.not.been.called(); + expect(collection.disposed).is.false; + + disposable2.dispose(); + expect(collection['disposables']).to.be.empty; + expect(collection.disposed).is.true; + expect(onDispose).to.have.been.called.once; + }); + + it('onDispose is only called once on actual disposal of elements', () => { + const onDispose = spy(() => { }); + const elementDispose = spy(() => { }); + + const collection = new DisposableCollection(); + collection.onDispose(onDispose); + + // if the collection is empty 'onDispose' is not called + collection.dispose(); + expect(onDispose).to.not.have.been.called(); + + // 'onDispose' is called because we actually dispose an element + collection.push(Disposable.create(elementDispose)); + collection.dispose(); + expect(elementDispose).to.have.been.called.once; + expect(onDispose).to.have.been.called.once; + + // if the collection is empty 'onDispose' is not called and no further element is disposed + collection.dispose(); + expect(elementDispose).to.have.been.called.once; + expect(onDispose).to.have.been.called.once; + + // 'onDispose' is not called again even if we actually dispose an element + collection.push(Disposable.create(elementDispose)); + collection.dispose(); + expect(elementDispose).to.have.been.called.twice; + expect(onDispose).to.have.been.called.once; + }); }); diff --git a/packages/core/src/common/disposable.ts b/packages/core/src/common/disposable.ts index 1920343c9073e..3eb776598fd48 100644 --- a/packages/core/src/common/disposable.ts +++ b/packages/core/src/common/disposable.ts @@ -47,6 +47,34 @@ Object.defineProperty(Disposable, 'NULL', { } }); +/** + * Utility for tracking a collection of Disposable objects. + * + * This utility provides a number of benefits over just using an array of + * Disposables: + * + * - the collection is auto-pruned when an element it contains is disposed by + * any code that has a reference to it + * - you can register to be notified when all elements in the collection have + * been disposed [1] + * - you can conveniently dispose all elements by calling dispose() + * on the collection + * + * Unlike an array, however, this utility does not give you direct access to + * its elements. + * + * Being notified when all elements are disposed is simple: + * ``` + * const dc = new DisposableCollection(myDisposables); + * dc.onDispose(() => { + * console.log('All elements in the collection have been disposed'); + * }); + * ``` + * + * [1] The collection will notify only once. It will continue to function in so + * far as accepting new Disposables and pruning them when they are disposed, but + * such activity will never result in another notification. + */ export class DisposableCollection implements Disposable { protected readonly disposables: Disposable[] = []; @@ -133,3 +161,28 @@ export function disposableTimeout(...args: Parameters): Dispo const handle = setTimeout(...args); return { dispose: () => clearTimeout(handle) }; } + +/** + * Wrapper for a {@link Disposable} that is not available immediately. + */ +export class DisposableWrapper implements Disposable { + + private disposed = false; + private disposable: Disposable | undefined = undefined; + + set(disposable: Disposable): void { + if (this.disposed) { + disposable.dispose(); + } else { + this.disposable = disposable; + } + } + + dispose(): void { + this.disposed = true; + if (this.disposable) { + this.disposable.dispose(); + this.disposable = undefined; + } + } +} diff --git a/packages/core/src/common/encoding-service.ts b/packages/core/src/common/encoding-service.ts index 529298873d856..1e4aaafe8d41a 100644 --- a/packages/core/src/common/encoding-service.ts +++ b/packages/core/src/common/encoding-service.ts @@ -318,7 +318,7 @@ export class EncodingService { }); } - encodeStream(value: string | Readable, options?: ResourceEncoding): Promise + encodeStream(value: string | Readable, options?: ResourceEncoding): Promise; encodeStream(value?: string | Readable, options?: ResourceEncoding): Promise; async encodeStream(value: string | Readable | undefined, options?: ResourceEncoding): Promise { let encoding = options?.encoding; diff --git a/packages/core/src/common/event.ts b/packages/core/src/common/event.ts index 99fdef2ea5fde..94912d7616455 100644 --- a/packages/core/src/common/event.ts +++ b/packages/core/src/common/event.ts @@ -89,6 +89,12 @@ export namespace Event { return new Promise(resolve => once(event)(resolve)); } + export function filter(event: Event, predicate: (e: T) => unknown): Event; + export function filter(event: Event, predicate: (e: T) => e is S): Event; + export function filter(event: Event, predicate: (e: T) => unknown): Event { + return (listener, thisArg, disposables) => event(e => predicate(e) && listener.call(thisArg, e), undefined, disposables); + } + /** * Given an event and a `map` function, returns another event which maps each element * through the mapping function. @@ -467,3 +473,21 @@ export class AsyncEmitter extends Emitter { } } + +export class QueueableEmitter extends Emitter { + + currentQueue?: T[]; + + queue(...arg: T[]): void { + if (!this.currentQueue) { + this.currentQueue = []; + } + this.currentQueue.push(...arg); + } + + override fire(): void { + super.fire(this.currentQueue || []); + this.currentQueue = undefined; + } + +} diff --git a/packages/core/src/node/file-uri.ts b/packages/core/src/common/file-uri.ts similarity index 97% rename from packages/core/src/node/file-uri.ts rename to packages/core/src/common/file-uri.ts index 99a86fbb7b5ca..f92af9fc0fdaf 100644 --- a/packages/core/src/node/file-uri.ts +++ b/packages/core/src/common/file-uri.ts @@ -15,8 +15,8 @@ // ***************************************************************************** import { URI as Uri } from 'vscode-uri'; -import URI from '../common/uri'; -import { isWindows } from '../common/os'; +import URI from './uri'; +import { isWindows } from './os'; export namespace FileUri { diff --git a/packages/core/src/common/glob.ts b/packages/core/src/common/glob.ts index d32394ec90191..1676fc4234e45 100644 --- a/packages/core/src/common/glob.ts +++ b/packages/core/src/common/glob.ts @@ -454,10 +454,10 @@ function toRegExp(pattern: string): ParsedStringPattern { /** * Simplified glob matching. Supports a subset of glob patterns: - * - * matches anything inside a path segment - * - ? matches 1 character inside a path segment - * - ** matches anything including an empty path segment - * - simple brace expansion ({js,ts} => js or ts) + * - `*` matches anything inside a path segment + * - `?` matches 1 character inside a path segment + * - `**` matches anything including an empty path segment + * - simple brace expansion (`{js,ts}` => js or ts) * - character ranges (using [...]) */ export function match(pattern: string | IRelativePattern, path: string): boolean; diff --git a/packages/core/src/common/i18n/nls.metadata.json b/packages/core/src/common/i18n/nls.metadata.json index d04046e10b5b4..6afd015f8d03f 100644 --- a/packages/core/src/common/i18n/nls.metadata.json +++ b/packages/core/src/common/i18n/nls.metadata.json @@ -22,9 +22,6 @@ "vs/code/node/cliProcessMain": [ "cli" ], - "vs/code/node/sharedProcess/sharedProcessMain": [ - "sharedLog" - ], "vs/code/electron-sandbox/processExplorer/processExplorerMain": [ "name", "cpu", @@ -36,16 +33,10 @@ "copyAll", "debug" ], - "vs/workbench/electron-sandbox/desktop.main": [ - "join.closeStorage" + "vs/code/node/sharedProcess/sharedProcessMain": [ + "sharedLog" ], "vs/workbench/electron-sandbox/desktop.contribution": [ - "newTab", - "showPreviousTab", - "showNextWindowTab", - "moveWindowTabToNewWindow", - "mergeAllWindowTabs", - "toggleWindowTabsBar", { "key": "miExit", "comment": [ @@ -54,6 +45,7 @@ }, "application.shellEnvironmentResolutionTimeout", "windowConfigurationTitle", + "confirmSaveUntitledWorkspace", "window.openWithoutArgumentsInNewWindow.on", "window.openWithoutArgumentsInNewWindow.off", "openWithoutArgumentsInNewWindow", @@ -64,7 +56,18 @@ "window.reopenFolders.none", "restoreWindows", "restoreFullscreen", - "zoomLevel", + { + "comment": [ + "{0} will be a setting name rendered as a link" + ], + "key": "zoomLevel" + }, + { + "comment": [ + "{0} will be a setting name rendered as a link" + ], + "key": "zoomPerWindow" + }, "window.newWindowDimensions.default", "window.newWindowDimensions.inherit", "window.newWindowDimensions.offset", @@ -74,6 +77,10 @@ "closeWhenEmpty", "window.doubleClickIconToClose", "titleBarStyle", + "window.customTitleBarVisibility.auto", + "window.customTitleBarVisibility.windowed", + "window.customTitleBarVisibility.never", + "window.customTitleBarVisibility", "dialogStyle", "window.nativeTabs", "window.nativeFullScreen", @@ -84,6 +91,8 @@ "keyboardConfigurationTitle", "touchbar.enabled", "touchbar.ignored", + "security.promptForLocalFileProtocolHandling", + "security.promptForRemoteFileProtocolHandling", "argv.locale", "argv.disableHardwareAcceleration", "argv.forceColorProfile", @@ -92,7 +101,18 @@ "argv.enebleProposedApi", "argv.logLevel", "argv.disableChromiumSandbox", - "argv.force-renderer-accessibility" + "argv.useInMemorySecretStorage", + "argv.force-renderer-accessibility", + "argv.passwordStore", + "newTab", + "showPreviousTab", + "showNextWindowTab", + "moveWindowTabToNewWindow", + "mergeAllWindowTabs", + "toggleWindowTabsBar" + ], + "vs/workbench/electron-sandbox/desktop.main": [ + "join.closeStorage" ], "vs/workbench/services/textfile/electron-sandbox/nativeTextFileService": [ "join.textFiles" @@ -112,6 +132,7 @@ "&& denotes a mnemonic" ] }, + "doNotAskAgain", "workspaceOpenedMessage", "workspaceOpenedDetail", "restartExtensionHost.reason" @@ -141,27 +162,14 @@ "local", "remote" ], + "vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService": [ + "join.workingCopyBackups" + ], "vs/workbench/services/integrity/electron-sandbox/integrityService": [ "integrity.prompt", "integrity.moreInformation", "integrity.dontShowAgain" ], - "vs/workbench/contrib/files/electron-sandbox/fileActions.contribution": [ - "revealInWindows", - "revealInMac", - "openContainer", - "miShare", - "filesCategory" - ], - "vs/workbench/services/voiceRecognition/electron-sandbox/workbenchVoiceRecognitionService": [ - "voiceTranscription", - "voiceTranscriptionGettingReady", - "voiceTranscriptionRecording", - "voiceTranscriptionError" - ], - "vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService": [ - "join.workingCopyBackups" - ], "vs/workbench/services/extensions/electron-sandbox/nativeExtensionService": [ "extensionService.versionMismatchCrash", "relaunch", @@ -177,37 +185,36 @@ "installResolver", "install", "resolverExtensionNotFound", - "restartExtensionHost", - "restartExtensionHost.reason" + "restartExtensionHost.reason", + "restartExtensionHost" ], - "vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution": [ - "runtimeExtension" + "vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService": [ + "backupErrorDetails" ], "vs/workbench/contrib/localization/electron-sandbox/localization.contribution": [ "updateLocale", "changeAndRestart", "neverAgain" ], - "vs/workbench/contrib/remote/electron-sandbox/remote.contribution": [ - "wslFeatureInstalled", - "remote", - "remote.downloadExtensionsLocally" + "vs/workbench/contrib/files/electron-sandbox/fileActions.contribution": [ + "miShare", + "revealInWindows", + "revealInMac", + "openContainer", + "filesCategory" + ], + "vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution": [ + "runtimeExtension" ], "vs/workbench/contrib/issue/electron-sandbox/issue.contribution": [ - { - "key": "reportPerformanceIssue", - "comment": [ - "Here, 'issue' means problem or bug" - ] - }, - "openProcessExplorer", + "tasksQuickAccessPlaceholder", + "openIssueReporter", { "key": "miOpenProcessExplorerer", "comment": [ "&& denotes a mnemonic" ] }, - "stopTracing", "stopTracing.message", { "key": "stopTracing.button", @@ -216,35 +223,43 @@ ] }, "stopTracing.title", - "stopTracing.detail" - ], - "vs/workbench/contrib/userDataSync/electron-sandbox/userDataSync.contribution": [ - "Open Backup folder", - "no backups", - "download sync activity complete", - "open" - ], - "vs/workbench/contrib/tasks/electron-sandbox/taskService": [ - "TaskSystem.runningTask", + "stopTracing.detail", { - "key": "TaskSystem.terminateTask", + "key": "reportPerformanceIssue", "comment": [ - "&& denotes a mnemonic" + "Here, 'issue' means problem or bug" ] }, - "TaskSystem.noProcess", + "openProcessExplorer", + "stopTracing" + ], + "vs/workbench/contrib/remote/electron-sandbox/remote.contribution": [ + "wslFeatureInstalled", + "remote", + "remote.downloadExtensionsLocally" + ], + "vs/workbench/services/themes/electron-sandbox/themes.contribution": [ + "window.systemColorTheme.default", + "window.systemColorTheme.auto", + "window.systemColorTheme.light", + "window.systemColorTheme.dark", { - "key": "TaskSystem.exitAnyways", + "key": "window.systemColorTheme", "comment": [ - "&& denotes a mnemonic" + "{0} and {1} will become links to other settings." ] } ], + "vs/workbench/contrib/userDataSync/electron-sandbox/userDataSync.contribution": [ + "no backups", + "download sync activity complete", + "open", + "Open Backup folder" + ], "vs/workbench/contrib/performance/electron-sandbox/performance.contribution": [ "experimental.rendererProfiling" ], "vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution": [ - "globalConsoleAction", "terminalConfigurationTitle", "terminal.explorerKind.integrated", "terminal.explorerKind.external", @@ -256,10 +271,29 @@ "sourceControlRepositories.openInTerminalKind", "terminal.external.windowsExec", "terminal.external.osxExec", - "terminal.external.linuxExec" + "terminal.external.linuxExec", + "globalConsoleAction" + ], + "vs/workbench/contrib/tasks/electron-sandbox/taskService": [ + "TaskSystem.runningTask", + { + "key": "TaskSystem.terminateTask", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "TaskSystem.noProcess", + { + "key": "TaskSystem.exitAnyways", + "comment": [ + "&& denotes a mnemonic" + ] + } + ], + "vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution": [ + "name" ], "vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution": [ - "remoteTunnel.category", "remoteTunnel.actions.turnOn", "remoteTunnel.actions.turnOff", "remoteTunnel.actions.showLog", @@ -344,7 +378,8 @@ "manage.tunnelName", "remoteTunnelAccess.machineName", "remoteTunnelAccess.machineNameRegex", - "remoteTunnelAccess.preventSleep" + "remoteTunnelAccess.preventSleep", + "remoteTunnel.category" ], "vs/base/common/platform": [ { @@ -354,6 +389,66 @@ ] } ], + "vs/platform/environment/node/argv": [ + "optionsUpperCase", + "extensionsManagement", + "troubleshooting", + "cliDataDir", + "cliDataDir", + "diff", + "merge", + "add", + "goto", + "newWindow", + "reuseWindow", + "wait", + "locale", + "userDataDir", + "profileName", + "help", + "extensionHomePath", + "listExtensions", + "showVersions", + "category", + "installExtension", + "install prerelease", + "uninstallExtension", + "updateExtensions", + "experimentalApis", + "version", + "verbose", + "log", + "status", + "prof-startup", + "disableExtensions", + "disableExtension", + "turn sync", + "inspect-extensions", + "inspect-brk-extensions", + "disableGPU", + "disableChromiumSandbox", + "telemetry", + "deprecated.useInstead", + "paths", + "usage", + "options", + "stdinWindows", + "stdinUnix", + "subcommands", + "unknownVersion", + "unknownCommit" + ], + "vs/platform/log/common/log": [ + "trace", + "debug", + "info", + "warn", + "error", + "off" + ], + "vs/platform/terminal/node/ptyService": [ + "terminal-history-restored" + ], "vs/editor/common/config/editorOptions": [ "accessibilitySupport.auto", "accessibilitySupport.on", @@ -405,7 +500,10 @@ "wrappingStrategy.simple", "wrappingStrategy.advanced", "wrappingStrategy", - "codeActions", + "editor.lightbulb.enabled.off", + "editor.lightbulb.enabled.onCode", + "editor.lightbulb.enabled.on", + "enabled", "editor.stickyScroll.enabled", "editor.stickyScroll.maxLineCount", "editor.stickyScroll.defaultModel", @@ -430,6 +528,9 @@ "minimap.scale", "minimap.renderCharacters", "minimap.maxColumn", + "minimap.showRegionSectionHeaders", + "minimap.showMarkSectionHeaders", + "minimap.sectionHeaderFontSize", "padding.top", "padding.bottom", "parameterHints.enabled", @@ -460,6 +561,7 @@ "scrollbar.verticalScrollbarSize", "scrollbar.horizontalScrollbarSize", "scrollbar.scrollByPage", + "scrollbar.ignoreHorizontalScrollbarInContentHeight", "unicodeHighlight.nonBasicASCII", "unicodeHighlight.invisibleCharacters", "unicodeHighlight.ambiguousCharacters", @@ -470,8 +572,17 @@ "inlineSuggest.enabled", "inlineSuggest.showToolbar.always", "inlineSuggest.showToolbar.onHover", + "inlineSuggest.showToolbar.never", "inlineSuggest.showToolbar", "inlineSuggest.suppressSuggestions", + "inlineSuggest.fontFamily", + "inlineEdit.enabled", + "inlineEdit.showToolbar.always", + "inlineEdit.showToolbar.onHover", + "inlineEdit.showToolbar.never", + "inlineEdit.showToolbar", + "inlineEdit.fontFamily", + "inlineEdit.backgroundColoring", "bracketPairColorization.enabled", "bracketPairColorization.independentColorPoolPerBracketType", "editor.guides.bracketPairs.true", @@ -538,6 +649,8 @@ "editor.suggest.showIssues", "selectLeadingAndTrailingWhitespace", "selectSubwords", + "wordSegmenterLocales", + "wordSegmenterLocales", "wrappingIndent.none", "wrappingIndent.same", "wrappingIndent.indent", @@ -627,6 +740,7 @@ "links", "matchBrackets", "mouseWheelScrollSensitivity", + "mouseWheelZoom.mac", "mouseWheelZoom", "multiCursorMergeOverlapping", "multiCursorModifier.ctrlCmd", @@ -642,6 +756,9 @@ "multiCursorPaste.full", "multiCursorPaste", "multiCursorLimit", + "occurrencesHighlight.off", + "occurrencesHighlight.singleFile", + "occurrencesHighlight.multiFile", "occurrencesHighlight", "overviewRulerBorder", "peekWidgetDefaultFocus.tree", @@ -731,57 +848,6 @@ "defaultColorDecorators", "tabFocusMode" ], - "vs/platform/environment/node/argv": [ - "optionsUpperCase", - "extensionsManagement", - "troubleshooting", - "cliDataDir", - "cliDataDir", - "diff", - "merge", - "add", - "goto", - "newWindow", - "reuseWindow", - "wait", - "locale", - "userDataDir", - "profileName", - "help", - "extensionHomePath", - "listExtensions", - "showVersions", - "category", - "installExtension", - "install prerelease", - "uninstallExtension", - "experimentalApis", - "version", - "verbose", - "log", - "status", - "prof-startup", - "disableExtensions", - "disableExtension", - "turn sync", - "inspect-extensions", - "inspect-brk-extensions", - "disableGPU", - "disableChromiumSandbox", - "telemetry", - "deprecated.useInstead", - "paths", - "usage", - "options", - "stdinWindows", - "stdinUnix", - "subcommands", - "unknownVersion", - "unknownCommit" - ], - "vs/platform/terminal/node/ptyService": [ - "terminal-history-restored" - ], "vs/base/common/errorMessage": [ "stackTrace.format", "nodeExceptionMessage", @@ -791,6 +857,9 @@ "error.defaultMessage" ], "vs/code/electron-main/app": [ + "confirmOpenMessageWorkspace", + "confirmOpenMessageFolder", + "confirmOpenMessageFileOrFolder", { "key": "open", "comment": [ @@ -803,16 +872,9 @@ "&& denotes a mnemonic" ] }, - "confirmOpenMessage", - "confirmOpenDetail" - ], - "vs/platform/files/common/files": [ - "unknownError", - "sizeB", - "sizeKB", - "sizeMB", - "sizeGB", - "sizeTB" + "confirmOpenDetail", + "doNotAskAgainLocal", + "doNotAskAgainRemote" ], "vs/platform/environment/node/argvHelper": [ "multipleValues", @@ -822,14 +884,13 @@ "unknownOption", "gotoValidation" ], - "vs/platform/files/node/diskFileSystemProvider": [ - "fileExists", - "fileNotExists", - "moveError", - "copyError", - "fileCopyErrorPathCase", - "fileMoveCopyErrorNotFound", - "fileMoveCopyErrorExists" + "vs/platform/files/common/files": [ + "unknownError", + "sizeB", + "sizeKB", + "sizeMB", + "sizeGB", + "sizeTB" ], "vs/platform/files/common/fileService": [ "invalidPath", @@ -860,6 +921,15 @@ "err.readonly", "err.readonly" ], + "vs/platform/files/node/diskFileSystemProvider": [ + "fileExists", + "fileNotExists", + "moveError", + "copyError", + "fileCopyErrorPathCase", + "fileMoveCopyErrorNotFound", + "fileMoveCopyErrorExists" + ], "vs/platform/request/common/request": [ "request", "httpConfigurationTitle", @@ -875,6 +945,19 @@ "systemCertificates", "systemCertificatesV2" ], + "vs/platform/update/common/update.config.contribution": [ + "updateConfigurationTitle", + "updateMode", + "none", + "manual", + "start", + "default", + "updateMode", + "deprecated", + "enableWindowsBackgroundUpdatesTitle", + "enableWindowsBackgroundUpdates", + "showReleaseNotes" + ], "vs/platform/dialogs/common/dialogs": [ { "key": "yesButton", @@ -901,18 +984,91 @@ "moreFile", "moreFiles" ], - "vs/platform/update/common/update.config.contribution": [ - "updateConfigurationTitle", - "updateMode", - "none", - "manual", - "start", - "default", - "updateMode", - "deprecated", - "enableWindowsBackgroundUpdatesTitle", - "enableWindowsBackgroundUpdates", - "showReleaseNotes" + "vs/platform/extensionManagement/common/extensionManagement": [ + "extensions", + "preferences" + ], + "vs/platform/extensionManagement/common/extensionManagementCLI": [ + "notFound", + "useId", + "listFromLocation", + "installingExtensionsOnLocation", + "installingExtensions", + "error while installing extensions", + "installation failed", + { + "key": "updateExtensionsQuery", + "comment": [ + "Placeholder is for the count of extensions" + ] + }, + "updateExtensionsNoExtensions", + "updateExtensionsNewVersionsAvailable", + "errorUpdatingExtension", + "successUpdate", + "alreadyInstalled-checkAndUpdate", + "alreadyInstalled", + "alreadyInstalled", + "updateMessage", + "installing builtin with version", + "installing builtin ", + "installing with version", + "installing", + "errorInstallingExtension", + "successInstall", + "successVsixInstall", + "cancelVsixInstall", + "forceDowngrade", + "builtin", + "forceUninstall", + "uninstalling", + "successUninstallFromLocation", + "successUninstall", + "notInstalleddOnLocation", + "notInstalled" + ], + "vs/platform/extensionManagement/common/extensionsScannerService": [ + "fileReadFail", + "jsonParseFail", + "jsonParseInvalidType", + "jsonsParseReportErrors", + "jsonInvalidFormat", + "jsonsParseReportErrors", + "jsonInvalidFormat" + ], + "vs/platform/extensionManagement/node/extensionManagementService": [ + "incompatible", + "MarketPlaceDisabled", + "Not a Marketplace extension", + "removeError", + "errorDeleting", + "renameError", + "cannot read", + "restartCode", + "restartCode" + ], + "vs/platform/languagePacks/common/languagePacks": [ + "currentDisplayLanguage" + ], + "vs/platform/telemetry/common/telemetryService": [ + "telemetry.telemetryLevelMd", + "telemetry.docsStatement", + "telemetry.docsAndPrivacyStatement", + "telemetry.restart", + "telemetry.crashReports", + "telemetry.errors", + "telemetry.usage", + "telemetry.telemetryLevel.tableDescription", + "telemetry.telemetryLevel.deprecated", + "telemetryConfigurationTitle", + "telemetry.telemetryLevel.default", + "telemetry.telemetryLevel.error", + "telemetry.telemetryLevel.crash", + "telemetry.telemetryLevel.off", + "telemetryConfigurationTitle", + "telemetry.enableTelemetry", + "telemetry.enableTelemetryMd", + "enableTelemetryDeprecated" ], "vs/code/electron-sandbox/issue/issueReporterPage": [ "sendSystemInfo", @@ -920,6 +1076,7 @@ "sendWorkspaceInfo", "sendExtensions", "sendExperiments", + "sendExtensionData", { "key": "reviewGuidanceLabel", "comment": [ @@ -942,12 +1099,18 @@ "titleLengthValidation", "details", "descriptionEmptyValidation", + "descriptionTooShortValidation", + "show", + "extensionData", "show", "show", "show", "show", "show" ], + "vs/platform/userDataProfile/common/userDataProfile": [ + "defaultProfile" + ], "vs/code/electron-sandbox/issue/issueReporterService": [ "hide", "show", @@ -983,85 +1146,6 @@ "disabledExtensions", "noCurrentExperiments" ], - "vs/platform/extensionManagement/common/extensionManagement": [ - "extensions", - "preferences" - ], - "vs/platform/extensionManagement/common/extensionsScannerService": [ - "fileReadFail", - "jsonParseFail", - "jsonParseInvalidType", - "jsonsParseReportErrors", - "jsonInvalidFormat", - "jsonsParseReportErrors", - "jsonInvalidFormat" - ], - "vs/platform/languagePacks/common/languagePacks": [ - "currentDisplayLanguage" - ], - "vs/platform/extensionManagement/common/extensionManagementCLI": [ - "notFound", - "useId", - "listFromLocation", - "installingExtensionsOnLocation", - "installingExtensions", - "alreadyInstalled-checkAndUpdate", - "alreadyInstalled", - "error while installing extensions", - "installation failed", - "successVsixInstall", - "cancelVsixInstall", - "alreadyInstalled", - "updateMessage", - "installing builtin with version", - "installing builtin ", - "installing with version", - "installing", - "successInstall", - "cancelInstall", - "forceDowngrade", - "builtin", - "forceUninstall", - "uninstalling", - "successUninstallFromLocation", - "successUninstall", - "notInstalleddOnLocation", - "notInstalled" - ], - "vs/platform/extensionManagement/node/extensionManagementService": [ - "incompatible", - "MarketPlaceDisabled", - "Not a Marketplace extension", - "removeError", - "errorDeleting", - "renameError", - "cannot read", - "restartCode", - "restartCode" - ], - "vs/platform/telemetry/common/telemetryService": [ - "telemetry.telemetryLevelMd", - "telemetry.docsStatement", - "telemetry.docsAndPrivacyStatement", - "telemetry.restart", - "telemetry.crashReports", - "telemetry.errors", - "telemetry.usage", - "telemetry.telemetryLevel.tableDescription", - "telemetry.telemetryLevel.deprecated", - "telemetryConfigurationTitle", - "telemetry.telemetryLevel.default", - "telemetry.telemetryLevel.error", - "telemetry.telemetryLevel.crash", - "telemetry.telemetryLevel.off", - "telemetryConfigurationTitle", - "telemetry.enableTelemetry", - "telemetry.enableTelemetryMd", - "enableTelemetryDeprecated" - ], - "vs/platform/userDataProfile/common/userDataProfile": [ - "defaultProfile" - ], "vs/platform/telemetry/common/telemetryLogAppender": [ "telemetryLog" ], @@ -1072,18 +1156,18 @@ "app.extension.identifier.errorMessage", "settingsSync.ignoredSettings" ], - "vs/platform/userDataSync/common/userDataSyncMachines": [ - "error incompatible" - ], "vs/platform/userDataSync/common/userDataSyncLog": [ "userDataSyncLog" ], - "vs/platform/userDataSync/common/userDataSyncResourceProvider": [ - "incompatible sync data" + "vs/platform/userDataSync/common/userDataSyncMachines": [ + "error incompatible" ], "vs/platform/remoteTunnel/common/remoteTunnel": [ "remoteTunnelLog" ], + "vs/platform/userDataSync/common/userDataSyncResourceProvider": [ + "incompatible sync data" + ], "vs/platform/remoteTunnel/node/remoteTunnelService": [ "remoteTunnelService.building", { @@ -1137,6 +1221,8 @@ "defaultFindMatchTypeSettingKey.contiguous", "defaultFindMatchTypeSettingKey", "expand mode", + "sticky scroll", + "sticky scroll maximum items", "typeNavigationMode2" ], "vs/platform/markers/common/markers": [ @@ -1164,41 +1250,61 @@ "workbench.editor.titleScrollbarSizing.default", "workbench.editor.titleScrollbarSizing.large", "tabScrollbarHeight", + "workbench.editor.showTabs.multiple", + "workbench.editor.showTabs.single", + "workbench.editor.showTabs.none", "showEditorTabs", - "wrapTabs", { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0} will be a setting name rendered as a link" ], - "key": "scrollToSwitchTabs" + "key": "workbench.editor.editorActionsLocation.default" }, - "highlightModifiedTabs", - "decorations.badges", - "decorations.colors", - "workbench.editor.labelFormat.default", - "workbench.editor.labelFormat.short", - "workbench.editor.labelFormat.medium", - "workbench.editor.labelFormat.long", { "comment": [ - "This is the description for a setting. Values surrounded by parenthesis are not to be translated." + "{0} will be a setting name rendered as a link" ], - "key": "tabDescription" + "key": "workbench.editor.editorActionsLocation.titleBar" }, - "workbench.editor.untitled.labelFormat.content", - "workbench.editor.untitled.labelFormat.name", + "workbench.editor.editorActionsLocation.hidden", + "editorActionsLocation", { "comment": [ - "This is the description for a setting. Values surrounded by parenthesis are not to be translated." + "{0}, {1} will be a setting name rendered as a link" ], - "key": "untitledLabelFormat" + "key": "wrapTabs" }, { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0}, {1} will be a setting name rendered as a link" + ], + "key": "scrollToSwitchTabs" + }, + { + "comment": [ + "{0}, {1} will be a setting name rendered as a link" ], - "key": "emptyEditorHint" + "key": "highlightModifiedTabs" }, + "decorations.badges", + "decorations.colors", + "workbench.editor.label.enabled", + "workbench.editor.label.patterns", + "workbench.editor.label.dirname", + "workbench.editor.label.nthdirname", + "workbench.editor.label.filename", + "workbench.editor.label.extname", + "customEditorLabelDescriptionExample", + "workbench.editor.label.template", + "workbench.editor.labelFormat.default", + "workbench.editor.labelFormat.short", + "workbench.editor.labelFormat.medium", + "workbench.editor.labelFormat.long", + "tabDescription", + "workbench.editor.untitled.labelFormat.content", + "workbench.editor.untitled.labelFormat.name", + "untitledLabelFormat", + "workbench.editor.empty.hint", "workbench.editor.languageDetection", "workbench.editor.historyBasedLanguageDetection", "workbench.editor.preferBasedLanguageDetection", @@ -1207,34 +1313,36 @@ "workbench.editor.showLanguageDetectionHints.notebook", { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0} will be a setting name rendered as a link" ], - "key": "editorTabCloseButton" + "key": "tabActionLocation" }, + "workbench.editor.tabActionCloseVisibility", + "workbench.editor.tabActionUnpinVisibility", "workbench.editor.tabSizing.fit", "workbench.editor.tabSizing.shrink", "workbench.editor.tabSizing.fixed", { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0}, {1} will be a setting name rendered as a link" ], "key": "tabSizing" }, { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0}, {1} will be a setting name rendered as a link" ], "key": "workbench.editor.tabSizingFixedMinWidth" }, { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0}, {1} will be a setting name rendered as a link" ], "key": "workbench.editor.tabSizingFixedMaxWidth" }, { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0}, {1} will be a setting name rendered as a link" ], "key": "workbench.editor.tabHeight" }, @@ -1243,11 +1351,16 @@ "workbench.editor.pinnedTabSizing.shrink", { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0}, {1} will be a setting name rendered as a link" ], "key": "pinnedTabSizing" }, - "workbench.editor.pinnedTabsOnSeparateRow", + { + "comment": [ + "{0}, {1} will be a setting name rendered as a link" + ], + "key": "workbench.editor.pinnedTabsOnSeparateRow" + }, "workbench.editor.preventPinnedEditorClose.always", "workbench.editor.preventPinnedEditorClose.onlyKeyboard", "workbench.editor.preventPinnedEditorClose.onlyMouse", @@ -1256,22 +1369,28 @@ "workbench.editor.splitSizingAuto", "workbench.editor.splitSizingDistribute", "workbench.editor.splitSizingSplit", - { - "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." - ], - "key": "splitSizing" - }, + "splitSizing", "splitOnDragAndDrop", + "dragToOpenWindow", "focusRecentEditorAfterClose", "showIcons", "enablePreview", - "enablePreviewFromQuickOpen", - "enablePreviewFromCodeNavigation", + { + "comment": [ + "{0}, {1} will be a setting name rendered as a link" + ], + "key": "enablePreviewFromQuickOpen" + }, + { + "comment": [ + "{0}, {1} will be a setting name rendered as a link" + ], + "key": "enablePreviewFromCodeNavigation" + }, "closeOnFileDelete", { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0}, {1}, {2}, {3} will be a setting name rendered as a link" ], "key": "editorOpenPositioning" }, @@ -1292,10 +1411,13 @@ "centeredLayoutDynamicWidth", { "comment": [ - "This is the description for a setting. Values surrounded by single quotes are not to be translated." + "{0}, {1} will be a setting name rendered as a link" ], "key": "doubleClickTabToToggleEditorGroupSizes" }, + "workbench.editor.doubleClickTabToToggleEditorGroupSizes.maximize", + "workbench.editor.doubleClickTabToToggleEditorGroupSizes.expand", + "workbench.editor.doubleClickTabToToggleEditorGroupSizes.off", "limitEditorsEnablement", "limitEditorsMaximum", "limitEditorsExcludeDirty", @@ -1324,8 +1446,22 @@ "workbench.panel.opensMaximized.never", "workbench.panel.opensMaximized.preserve", "statusBarVisibility", - "activityBarVisibility", - "activityBarIconClickBehavior", + { + "comment": [ + "This is the description for a setting" + ], + "key": "activityBarLocation" + }, + "workbench.activityBar.location.default", + "workbench.activityBar.location.top", + "workbench.activityBar.location.bottom", + "workbench.activityBar.location.hide", + { + "comment": [ + "{0}, {1} will be a setting name rendered as a link" + ], + "key": "activityBarIconClickBehavior" + }, "workbench.activityBar.iconClickBehavior.toggle", "workbench.activityBar.iconClickBehavior.focus", "viewVisibility", @@ -1346,7 +1482,7 @@ { "key": "layoutControlEnabled", "comment": [ - "{0} is a placeholder for a setting identifier." + "{0}, {1} is a placeholder for a setting identifier." ] }, "layoutcontrol.type.menu", @@ -1371,6 +1507,8 @@ "remoteName", "dirty", "focusedView", + "activeRepositoryName", + "activeRepositoryBranchName", "separator", "windowConfigurationTitle", "window.titleSeparator", @@ -1378,7 +1516,7 @@ { "key": "window.commandCenter", "comment": [ - "{0} is a placeholder for a setting identifier." + "{0}, {1} is a placeholder for a setting identifier." ] }, "window.menuBarVisibility.classic", @@ -1390,7 +1528,7 @@ { "key": "window.menuBarVisibility.compact", "comment": [ - "{0} is a placeholder for a setting identifier." + "{0}, {1} is a placeholder for a setting identifier." ] }, "menuBarVisibility.mac", @@ -1415,10 +1553,14 @@ "window.confirmBeforeClose.never", "confirmBeforeCloseWeb", "confirmBeforeClose", + "problems.visibility", "zenModeConfigurationTitle", "zenMode.fullScreen", "zenMode.centerLayout", - "zenMode.hideTabs", + "zenMode.showTabs", + "zenMode.showTabs.multiple", + "zenMode.showTabs.single", + "zenMode.showTabs.none", "zenMode.hideStatusBar", "zenMode.hideActivityBar", "zenMode.hideLineNumbers", @@ -1434,23 +1576,8 @@ "selectAll" ], "vs/workbench/browser/actions/developerActions": [ - "inspect context keys", - "toggle screencast mode", - { - "key": "logStorage", - "comment": [ - "A developer only action to log the contents of the storage for the current window." - ] - }, "storageLogDialogMessage", "storageLogDialogDetails", - { - "key": "logWorkingCopies", - "comment": [ - "A developer only action to log the working copies that exist." - ] - }, - "removeLargeStorageDatabaseEntries", "largeStorageItemDetail", "global", "profile", @@ -1468,9 +1595,6 @@ "&& denotes a mnemonic" ] }, - "startTrackDisposables", - "snapshotTrackedDisposables", - "stopTrackDisposables", "screencastModeConfigurationTitle", "screencastMode.location.verticalPosition", "screencastMode.fontSize", @@ -1482,66 +1606,84 @@ "screencastMode.keyboardOptions.showSingleEditorCursorMoves", "screencastMode.keyboardOverlayTimeout", "screencastMode.mouseIndicatorColor", - "screencastMode.mouseIndicatorSize" + "screencastMode.mouseIndicatorSize", + "inspect context keys", + "toggle screencast mode", + { + "key": "logStorage", + "comment": [ + "A developer only action to log the contents of the storage for the current window." + ] + }, + { + "key": "logWorkingCopies", + "comment": [ + "A developer only action to log the working copies that exist." + ] + }, + "removeLargeStorageDatabaseEntries", + "startTrackDisposables", + "snapshotTrackedDisposables", + "stopTrackDisposables" ], "vs/workbench/browser/actions/helpActions": [ - "keybindingsReference", { "key": "miKeyboardShortcuts", "comment": [ "&& denotes a mnemonic" ] }, - "openVideoTutorialsUrl", { "key": "miVideoTutorials", "comment": [ "&& denotes a mnemonic" ] }, - "openTipsAndTricksUrl", { "key": "miTipsAndTricks", "comment": [ "&& denotes a mnemonic" ] }, - "openDocumentationUrl", { "key": "miDocumentation", "comment": [ "&& denotes a mnemonic" ] }, - "newsletterSignup", - "openYouTubeUrl", { "key": "miYouTube", "comment": [ "&& denotes a mnemonic" ] }, - "openUserVoiceUrl", { "key": "miUserVoice", "comment": [ "&& denotes a mnemonic" ] }, - "openLicenseUrl", { "key": "miLicense", "comment": [ "&& denotes a mnemonic" ] }, - "openPrivacyStatement", { "key": "miPrivacyStatement", "comment": [ "&& denotes a mnemonic" ] - } + }, + "keybindingsReference", + "openVideoTutorialsUrl", + "openTipsAndTricksUrl", + "openDocumentationUrl", + "newsletterSignup", + "openYouTubeUrl", + "openUserVoiceUrl", + "openLicenseUrl", + "openPrivacyStatement" ], "vs/workbench/browser/actions/layoutActions": [ "menuBarIcon", @@ -1560,27 +1702,15 @@ "fullScreenIcon", "centerLayoutIcon", "zenModeIcon", - "closeSidebar", - "toggleActivityBar", - { - "key": "miActivityBar", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "toggleCenteredLayout", { "key": "miToggleCenteredLayout", "comment": [ "&& denotes a mnemonic" ] }, - "moveSidebarRight", - "moveSidebarLeft", "toggleSidebarPosition", "moveSidebarRight", "moveSidebarLeft", - "toggleSidebarPosition", "cofigureLayoutIcon", "configureLayout", "move side bar right", @@ -1601,7 +1731,6 @@ "&& denotes a mnemonic" ] }, - "toggleEditor", { "key": "miShowEditorArea", "comment": [ @@ -1614,7 +1743,6 @@ "&& denotes a mnemonic" ] }, - "toggleSidebar", "primary sidebar", { "key": "primary sidebar mnemonic", @@ -1626,23 +1754,21 @@ "compositePart.hideSideBarLabel", "toggleSideBar", "toggleSideBar", - "toggleStatusbar", { "key": "miStatusbar", "comment": [ "&& denotes a mnemonic" ] }, - "toggleTabs", - "toggleSeparatePinnedEditorTabs", - "toggleZenMode", + "tabBar", + "tabBar", + "editorActionsPosition", { "key": "miToggleZenMode", "comment": [ "&& denotes a mnemonic" ] }, - "toggleMenuBar", { "key": "miMenuBar", "comment": [ @@ -1650,13 +1776,10 @@ ] }, "miMenuBarNoMnemonic", - "resetViewLocations", - "moveView", "sidebarContainer", "panelContainer", "secondarySideBarContainer", "moveFocusedView.selectView", - "moveFocusedView", "moveFocusedView.error.noFocusedView", "moveFocusedView.error.nonMovableView", "moveFocusedView.selectDestination", @@ -1677,14 +1800,7 @@ "sidebar", "panel", "secondarySideBar", - "resetFocusedViewLocation", "resetFocusedView.error.noFocusedView", - "increaseViewSize", - "increaseEditorWidth", - "increaseEditorHeight", - "decreaseViewSize", - "decreaseEditorWidth", - "decreaseEditorHeight", "selectToHide", "selectToShow", "active", @@ -1703,14 +1819,56 @@ "fullscreen", "zenMode", "centeredLayout", - "customizeLayout", "toggleVisibility", "sideBarPosition", "panelAlignment", "layoutModes", "customizeLayoutQuickPickTitle", "close", - "restore defaults" + "restore defaults", + "closeSidebar", + "toggleCenteredLayout", + "moveSidebarRight", + "moveSidebarLeft", + "toggleSidebarPosition", + "toggleEditor", + "toggleSidebar", + "toggleStatusbar", + "hideEditorTabs", + "hideEditorTabsDescription", + "hideEditorTabsZenMode", + "hideEditorTabsZenModeDescription", + "showMultipleEditorTabs", + "showMultipleEditorTabsDescription", + "showMultipleEditorTabsZenMode", + "showMultipleEditorTabsZenModeDescription", + "showSingleEditorTab", + "showSingleEditorTabDescription", + "showSingleEditorTabZenMode", + "showSingleEditorTabZenModeDescription", + "moveEditorActionsToTitleBar", + "moveEditorActionsToTitleBarDescription", + "moveEditorActionsToTabBar", + "moveEditorActionsToTabBarDescription", + "hideEditorActons", + "hideEditorActonsDescription", + "showEditorActons", + "showEditorActonsDescription", + "toggleSeparatePinnedEditorTabs", + "toggleSeparatePinnedEditorTabsDescription", + "toggleZenMode", + "toggleMenuBar", + "resetViewLocations", + "moveView", + "moveFocusedView", + "resetFocusedViewLocation", + "increaseViewSize", + "increaseEditorWidth", + "increaseEditorHeight", + "decreaseViewSize", + "decreaseEditorWidth", + "decreaseEditorHeight", + "customizeLayout" ], "vs/workbench/browser/actions/navigationActions": [ "navigateLeft", @@ -1720,6 +1878,16 @@ "focusNextPart", "focusPreviousPart" ], + "vs/workbench/browser/actions/listCommands": [ + { + "key": "mitoggleTreeStickyScroll", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "toggleTreeStickyScrollDescription", + "toggleTreeStickyScroll" + ], "vs/workbench/browser/actions/windowActions": [ "remove", "dirtyRecentlyOpenedFolder", @@ -1737,57 +1905,57 @@ "dirtyFolderConfirmDetail", "recentDirtyWorkspaceAriaLabel", "recentDirtyFolderAriaLabel", - "openRecent", { "key": "miMore", "comment": [ "&& denotes a mnemonic" ] }, - "quickOpenRecent", - "toggleFullScreen", { "key": "miToggleFullScreen", "comment": [ "&& denotes a mnemonic" ] }, - "reloadWindow", - "about", { "key": "miAbout", "comment": [ "&& denotes a mnemonic" ] }, - "newWindow", { "key": "miNewWindow", "comment": [ "&& denotes a mnemonic" ] }, - "blur", "miConfirmClose", { "key": "miOpenRecent", "comment": [ "&& denotes a mnemonic" ] - } + }, + "openRecent", + "quickOpenRecent", + "toggleFullScreen", + "reloadWindow", + "about", + "newWindow", + "blur" + ], + "vs/workbench/browser/actions/workspaceCommands": [ + { + "key": "add", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "addFolderToWorkspaceTitle", + "workspaceFolderPickerPlaceholder", + "addFolderToWorkspace" ], "vs/workbench/browser/actions/workspaceActions": [ - "workspaces", - "openFile", - "openFolder", - "openFolder", - "openFileFolder", - "openWorkspaceAction", - "closeWorkspace", - "openWorkspaceConfigFile", - "globalRemoveFolderFromWorkspace", - "saveWorkspaceAsAction", - "duplicateWorkspaceInNewWindow", { "key": "miOpenFile", "comment": [ @@ -1837,22 +2005,22 @@ "comment": [ "&& denotes a mnemonic" ] - } - ], - "vs/workbench/browser/actions/workspaceCommands": [ - "addFolderToWorkspace", - { - "key": "add", - "comment": [ - "&& denotes a mnemonic" - ] }, - "addFolderToWorkspaceTitle", - "workspaceFolderPickerPlaceholder" + "workspaces", + "openFile", + "openFolder", + "openFolder", + "openFileFolder", + "openWorkspaceAction", + "closeWorkspace", + "openWorkspaceConfigFile", + "globalRemoveFolderFromWorkspace", + "saveWorkspaceAsAction", + "duplicateWorkspaceInNewWindow" ], "vs/workbench/browser/actions/quickAccessActions": [ - "quickOpen", "quickOpenWithModes", + "quickOpen", "quickNavigateNext", "quickNavigatePrevious", "quickSelectNext", @@ -1873,14 +2041,25 @@ "menus.debugCallstackContext", "menus.debugVariablesContext", "menus.debugToolBar", + "menus.notebookVariablesContext", "menus.home", "menus.opy", "menus.scmTitle", "menus.scmSourceControl", + "menus.scmSourceControlTitle", "menus.resourceStateContext", "menus.resourceFolderContext", "menus.resourceGroupContext", "menus.changeTitle", + "menus.input", + "menus.incomingChanges", + "menus.incomingChangesContext", + "menus.outgoingChanges", + "menus.outgoingChangesContext", + "menus.incomingChangesAllChangesContext", + "menus.incomingChangesHistoryItemContext", + "menus.outgoingChangesAllChangesContext", + "menus.outgoingChangesHistoryItemContext", "menus.statusBarRemoteIndicator", "menus.terminalContext", "menus.terminalTabContext", @@ -1894,14 +2073,17 @@ "comment.title", "comment.actions", "comment.commentContext", + "commentsView.threadActions", "notebook.toolbar", "notebook.kernelSource", "notebook.cell.title", "notebook.cell.execute", "interactive.toolbar", "interactive.cell.title", + "issue.reporter", "testing.item.context", "testing.item.gutter.title", + "testing.item.result.title", "testing.message.context.title", "testing.message.content.title", "menus.extensionContext", @@ -1914,9 +2096,13 @@ "webview.context", "menus.share", "inlineCompletions.actions", + "inlineEdit.actions", "merge.toolbar", "editorLineNumberContext", "menus.mergeEditorResult", + "menus.multiDiffEditorResource", + "menus.diffEditorGutterToolBarMenus", + "menus.diffEditorGutterToolBarMenus", "requirestring", "optstring", "optstring", @@ -1980,7 +2166,12 @@ "dupe.command", "unsupported.submenureference", "missing.submenu", - "submenuItem.duplicate" + "submenuItem.duplicate", + "command name", + "command title", + "keyboard shortcuts", + "menuContexts", + "commands" ], "vs/workbench/api/common/configurationExtensionPoint": [ "vscode.extension.contributes.configuration.title", @@ -2024,7 +2215,11 @@ "workspaceConfig.extensions.description", "workspaceConfig.remoteAuthority", "workspaceConfig.transient", - "unknownWorkspaceProperty" + "unknownWorkspaceProperty", + "setting name", + "description", + "default", + "settings" ], "vs/workbench/api/browser/viewsExtensionPoint": [ { @@ -2051,6 +2246,7 @@ "vscode.extension.contributes.view.initialState.hidden", "vscode.extension.contributes.view.initialState.collapsed", "vscode.extension.contributs.view.size", + "vscode.extension.contributes.view.accessibilityHelpContent", "vscode.extension.contributes.view.id", "vscode.extension.contributes.view.name", "vscode.extension.contributes.view.when", @@ -2080,7 +2276,15 @@ "optstring", "optstring", "optstring", - "optenum" + "optenum", + "view container id", + "view container title", + "view container location", + "view id", + "view name title", + "view container location", + "viewsContainers", + "views" ], "vs/workbench/browser/parts/editor/editor.contribution": [ "textEditor", @@ -2093,20 +2297,34 @@ "allEditorsByAppearanceQuickAccess", "editorQuickAccessPlaceholder", "allEditorsByMostRecentlyUsedQuickAccess", + "lockGroupAction", "unlockGroupAction", "closeGroupAction", "splitUp", "splitDown", "splitLeft", "splitRight", + "newWindow", "toggleLockGroup", "close", "splitUp", "splitDown", "splitLeft", "splitRight", - "toggleTabs", - "toggleSeparatePinnedEditorTabs", + "moveEditorGroupToNewWindow", + "copyEditorGroupToNewWindow", + "tabBar", + "multipleTabs", + "singleTab", + "hideTabs", + "tabBar", + "multipleTabs", + "singleTab", + "hideTabs", + "editorActionsPosition", + "tabBar", + "titleBar", + "hidden", "close", "closeOthers", "closeRight", @@ -2122,11 +2340,15 @@ "splitRight", "splitInGroup", "joinInGroup", + "moveToNewWindow", + "copyToNewWindow", "inlineView", "showOpenedEditors", "closeAll", "closeAllSaved", "togglePreviewMode", + "maximizeGroup", + "unmaximizeGroup", "lockGroup", "splitEditorRight", "splitEditorDown", @@ -2141,24 +2363,15 @@ "close", "unpin", "close", + "lockEditorGroup", "unlockEditorGroup", "previousChangeIcon", - "nextChangeIcon", - "toggleWhitespace", "navigate.prev.label", + "nextChangeIcon", "navigate.next.label", + "swapDiffSides", + "toggleWhitespace", "ignoreTrimWhitespace.label", - "keepEditor", - "pinEditor", - "unpinEditor", - "closeEditor", - "closePinnedEditor", - "closeEditorsInGroup", - "closeSavedEditors", - "closeOtherEditors", - "closeRightEditors", - "closeEditorGroup", - "reopenWith", { "key": "miReopenClosedEditor", "comment": [ @@ -2178,98 +2391,96 @@ "&& denotes a mnemonic" ] }, - "miSplitEditorUpWithoutMnemonic", { "key": "miSplitEditorUp", "comment": [ "&& denotes a mnemonic" ] }, - "miSplitEditorDownWithoutMnemonic", { "key": "miSplitEditorDown", "comment": [ "&& denotes a mnemonic" ] }, - "miSplitEditorLeftWithoutMnemonic", { "key": "miSplitEditorLeft", "comment": [ "&& denotes a mnemonic" ] }, - "miSplitEditorRightWithoutMnemonic", { "key": "miSplitEditorRight", "comment": [ "&& denotes a mnemonic" ] }, - "miSplitEditorInGroupWithoutMnemonic", { "key": "miSplitEditorInGroup", "comment": [ "&& denotes a mnemonic" ] }, - "miJoinEditorInGroupWithoutMnemonic", { "key": "miJoinEditorInGroup", "comment": [ "&& denotes a mnemonic" ] }, - "miSingleColumnEditorLayoutWithoutMnemonic", + { + "key": "miMoveEditorToNewWindow", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miCopyEditorToNewWindow", + "comment": [ + "&& denotes a mnemonic" + ] + }, { "key": "miSingleColumnEditorLayout", "comment": [ "&& denotes a mnemonic" ] }, - "miTwoColumnsEditorLayoutWithoutMnemonic", { "key": "miTwoColumnsEditorLayout", "comment": [ "&& denotes a mnemonic" ] }, - "miThreeColumnsEditorLayoutWithoutMnemonic", { "key": "miThreeColumnsEditorLayout", "comment": [ "&& denotes a mnemonic" ] }, - "miTwoRowsEditorLayoutWithoutMnemonic", { "key": "miTwoRowsEditorLayout", "comment": [ "&& denotes a mnemonic" ] }, - "miThreeRowsEditorLayoutWithoutMnemonic", { "key": "miThreeRowsEditorLayout", "comment": [ "&& denotes a mnemonic" ] }, - "miTwoByTwoGridEditorLayoutWithoutMnemonic", { "key": "miTwoByTwoGridEditorLayout", "comment": [ "&& denotes a mnemonic" ] }, - "miTwoRowsRightEditorLayoutWithoutMnemonic", { "key": "miTwoRowsRightEditorLayout", "comment": [ "&& denotes a mnemonic" ] }, - "miTwoColumnsBottomEditorLayoutWithoutMnemonic", { "key": "miTwoColumnsBottomEditorLayout", "comment": [ @@ -2419,26 +2630,43 @@ "comment": [ "&& denotes a mnemonic" ] - } - ], - "vs/workbench/browser/parts/statusbar/statusbarPart": [ - "hideStatusBar" + }, + "keepEditor", + "pinEditor", + "unpinEditor", + "closeEditor", + "closePinnedEditor", + "closeEditorsInGroup", + "closeSavedEditors", + "closeOtherEditors", + "closeRightEditors", + "closeEditorGroup", + "reopenWith", + "miSplitEditorUpWithoutMnemonic", + "miSplitEditorDownWithoutMnemonic", + "miSplitEditorLeftWithoutMnemonic", + "miSplitEditorRightWithoutMnemonic", + "miSplitEditorInGroupWithoutMnemonic", + "miJoinEditorInGroupWithoutMnemonic", + "moveEditorToNewWindow", + "copyEditorToNewWindow", + "miSingleColumnEditorLayoutWithoutMnemonic", + "miTwoColumnsEditorLayoutWithoutMnemonic", + "miThreeColumnsEditorLayoutWithoutMnemonic", + "miTwoRowsEditorLayoutWithoutMnemonic", + "miThreeRowsEditorLayoutWithoutMnemonic", + "miTwoByTwoGridEditorLayoutWithoutMnemonic", + "miTwoRowsRightEditorLayoutWithoutMnemonic", + "miTwoColumnsBottomEditorLayoutWithoutMnemonic" ], "vs/workbench/browser/parts/banner/bannerPart": [ "focusBanner" ], - "vs/workbench/browser/parts/views/viewsService": [ - "show view", - "toggle view", - "show view", - "toggle view", - { - "key": "focus view", - "comment": [ - "{0} indicates the name of the view to be focused." - ] - }, - "resetViewLocation" + "vs/workbench/browser/parts/editor/editorParts": [ + "groupLabel" + ], + "vs/workbench/browser/parts/statusbar/statusbarPart": [ + "hideStatusBar" ], "vs/platform/undoRedo/common/undoRedoService": [ { @@ -2556,32 +2784,18 @@ "&& denotes a mnemonic" ] }, - "installAndHandle", "installDetail", - { - "key": "install and open", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "Installing", - "enableAndHandle", - { - "key": "enableAndReload", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "reloadAndHandle", + "openUri", + "reloadAndHandle", { "key": "reloadAndOpen", "comment": [ "&& denotes a mnemonic" ] }, + "no", "manage", - "extensions", - "no" + "extensions" ], "vs/workbench/services/keybinding/common/keybindingEditing": [ "errorKeybindingsFileDirty", @@ -2673,6 +2887,12 @@ "vscode.extension.contributes.languages.icon", "vscode.extension.contributes.languages.icon.light", "vscode.extension.contributes.languages.icon.dark", + "language id", + "language name", + "file extensions", + "grammar", + "snippets", + "languages", "invalid", "invalid.empty", "require.id", @@ -2723,20 +2943,16 @@ "workspace folder", "workspace" ], + "vs/workbench/services/extensionManagement/common/extensionFeaturesManagemetService": [ + "accessExtensionFeature", + "accessExtensionFeatureMessage", + "allow", + "disallow" + ], "vs/workbench/services/notification/common/notificationService": [ "neverShowAgain", "neverShowAgain" ], - "vs/workbench/services/userDataProfile/browser/userDataProfileManagement": [ - "reload message when updated", - "reload message when removed", - "reload message when removed", - "cannotRenameDefaultProfile", - "cannotDeleteDefaultProfile", - "switch profile", - "reload message", - "reload button" - ], "vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService": [ "profile import error", "resolving uri", @@ -2843,6 +3059,26 @@ "export profile title", "profile name required" ], + "vs/workbench/services/userDataProfile/browser/userDataProfileManagement": [ + "reload message when updated", + "reload message when removed", + "reload message when removed", + "cannotRenameDefaultProfile", + "cannotDeleteDefaultProfile", + "switch profile", + "reload message", + "reload button" + ], + "vs/workbench/services/remote/common/remoteExplorerService": [ + "getStartedWalkthrough.id", + "RemoteHelpInformationExtPoint", + "RemoteHelpInformationExtPoint.getStarted", + "RemoteHelpInformationExtPoint.documentation", + "RemoteHelpInformationExtPoint.feedback", + "RemoteHelpInformationExtPoint.feedback.deprecated", + "RemoteHelpInformationExtPoint.reportIssue", + "RemoteHelpInformationExtPoint.issues" + ], "vs/workbench/services/filesConfiguration/common/filesConfigurationService": [ "providerReadonly", { @@ -2872,6 +3108,20 @@ "hideView", "resetViewLocation" ], + "vs/workbench/services/views/browser/viewsService": [ + "editor", + "show view", + "toggle view", + "show view", + "toggle view", + { + "key": "focus view", + "comment": [ + "{0} indicates the name of the view to be focused." + ] + }, + "resetViewLocation" + ], "vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService": [ "no authentication providers", "no account", @@ -2925,19 +3175,11 @@ "sign in using account" ], "vs/workbench/services/authentication/browser/authenticationService": [ - "authentication.id", - "authentication.label", - { - "key": "authenticationExtensionPoint", - "comment": [ - "'Contributes' means adds here" - ] - }, - "authentication.Placeholder", "authentication.missingId", "authentication.missingLabel", - "authentication.idConflict", - "loading", + "authentication.idConflict" + ], + "vs/workbench/services/authentication/browser/authenticationExtensionsService": [ "sign in", "confirmAuthenticationAccess", { @@ -2976,73 +3218,6 @@ "vs/workbench/services/assignment/common/assignmentService": [ "workbench.enableExperiments" ], - "vs/workbench/contrib/preferences/browser/preferences.contribution": [ - "settingsEditor2", - "keybindingsEditor", - "openSettings2", - "openUserSettingsJson", - "openApplicationSettingsJson", - "preferences", - "settings", - { - "key": "miOpenSettings", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "openSettings2", - "openGlobalSettings", - "openRawDefaultSettings", - "openWorkspaceSettings", - "openAccessibilitySettings", - "openWorkspaceSettingsFile", - "openFolderSettings", - "openFolderSettingsFile", - "openFolderSettings", - { - "key": "miOpenOnlineSettings", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "filterUntrusted", - { - "key": "miOpenTelemetrySettings", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "openRemoteSettings", - "openRemoteSettingsJSON", - "settings.focusSearch", - "settings.clearResults", - "settings.focusFile", - "settings.focusFile", - "settings.focusSettingsList", - "settings.focusSettingsTOC", - "settings.focusSettingControl", - "settings.showContextMenu", - "settings.focusLevelUp", - "preferences", - "openGlobalKeybindings", - "keyboardShortcuts", - "keyboardShortcuts", - "openDefaultKeybindingsFile", - "openGlobalKeybindingsFile", - "showDefaultKeybindings", - "showExtensionKeybindings", - "showUserKeybindings", - "clear", - "clearHistory", - "defineKeybinding.start", - "openSettingsJson", - { - "key": "miPreferences", - "comment": [ - "&& denotes a mnemonic" - ] - } - ], "vs/workbench/services/issue/browser/issueTroubleshoot": [ "troubleshoot issue", "detail.start", @@ -3090,12 +3265,93 @@ ] } ], + "vs/workbench/contrib/preferences/browser/preferences.contribution": [ + "settingsEditor2", + "keybindingsEditor", + { + "key": "miOpenSettings", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "openFolderSettings", + { + "key": "miOpenOnlineSettings", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miOpenTelemetrySettings", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "settings.focusFile", + "settings.focusFile", + "settings.focusSettingsList", + "settings.focusSettingControl", + "keyboardShortcuts", + "keyboardShortcuts", + "clear", + "clearHistory", + { + "key": "miPreferences", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "openSettings2", + "openUserSettingsJson", + "openApplicationSettingsJson", + "preferences", + "settings", + "openSettings2", + "workbench.action.openSettingsJson.description", + "openGlobalSettings", + "openRawDefaultSettings", + "openWorkspaceSettings", + "openAccessibilitySettings", + "openWorkspaceSettingsFile", + "openFolderSettings", + "openFolderSettingsFile", + "filterUntrusted", + "openRemoteSettings", + "openRemoteSettingsJSON", + "settings.focusSearch", + "settings.clearResults", + "settings.focusSettingsTOC", + "settings.showContextMenu", + "settings.focusLevelUp", + "preferences", + "openGlobalKeybindings", + "openDefaultKeybindingsFile", + "openGlobalKeybindingsFile", + "showDefaultKeybindings", + "showExtensionKeybindings", + "showUserKeybindings", + "defineKeybinding.start", + "openSettingsJson" + ], "vs/workbench/contrib/performance/browser/performance.contribution": [ "show.label", "cycles", "insta.trace", "emitter" ], + "vs/workbench/contrib/chat/browser/chat.contribution": [ + "interactiveSessionConfigurationTitle", + "interactiveSession.editor.fontSize", + "interactiveSession.editor.fontFamily", + "interactiveSession.editor.fontWeight", + "interactiveSession.editor.wordWrap", + "interactiveSession.editor.lineHeight", + "chat.experimental.implicitContext", + "chat", + "chat", + "clear", + "file" + ], "vs/workbench/contrib/notebook/browser/notebook.contribution": [ "notebook.editorOptions.experimentalCustomization", "notebookConfigurationTitle", @@ -3118,7 +3374,10 @@ "insertToolbarLocation.both", "insertToolbarLocation.hidden", "notebook.globalToolbar.description", - "notebook.stickyScroll.description", + "notebook.stickyScrollEnabled.description", + "notebook.stickyScrollMode.description", + "notebook.stickyScrollMode.flat", + "notebook.stickyScrollMode.indented", "notebook.consolidatedOutputButton.description", "notebook.showFoldingControls.description", "showFoldingControls.always", @@ -3128,6 +3387,8 @@ "notebook.consolidatedRunButton.description", "notebook.globalToolbarShowLabel", "notebook.textOutputLineLimit", + "notebook.disableOutputFilePathLinks", + "notebook.minimalErrorRendering", "notebook.markup.fontSize", "notebook.interactiveWindow.collapseCodeCells", "notebook.outputLineHeight", @@ -3136,6 +3397,7 @@ "notebook.outputScrolling", "notebook.outputWordWrap", "notebook.formatOnSave", + "notebook.insertFinalNewline", "notebook.codeActionsOnSave", "explicit", "never", @@ -3149,56 +3411,28 @@ "notebook.scrolling.revealNextCellOnExecute.fullCell.description", "notebook.scrolling.revealNextCellOnExecute.firstLine.description", "notebook.scrolling.revealNextCellOnExecute.none.description", - "notebook.scrolling.anchorToFocusedCell.description", - "notebook.scrolling.anchorToFocusedCell.auto.description", - "notebook.scrolling.anchorToFocusedCell.on.description", - "notebook.scrolling.anchorToFocusedCell.off.description" - ], - "vs/workbench/contrib/chat/browser/chat.contribution": [ - "interactiveSessionConfigurationTitle", - "interactiveSession.editor.fontSize", - "interactiveSession.editor.fontFamily", - "interactiveSession.editor.fontWeight", - "interactiveSession.editor.wordWrap", - "interactiveSession.editor.lineHeight", - "chat", - "chat", - "clear" - ], - "vs/workbench/contrib/testing/browser/testing.contribution": [ - "test", - { - "key": "miViewTesting", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "testResultsPanelName", - "testResultsPanelName", - "noTestProvidersRegistered", - "searchForAdditionalTestExtensions", - "testExplorer" - ], - "vs/workbench/contrib/logs/common/logs.contribution": [ - "setDefaultLogLevel", - "remote name", - "show window log" + "notebook.cellChat", + "notebook.cellGenerate", + "notebook.VariablesView.description", + "notebook.cellFailureDiagnostics", + "notebook.backup.sizeLimit" ], "vs/workbench/contrib/interactive/browser/interactive.contribution": [ - "interactiveWindow", "interactive.open", + "interactiveScrollToTop", + "interactiveScrollToBottom", + "interactive.activeCodeBorder", + "interactive.inactiveCodeBorder", + "interactiveWindow.alwaysScrollOnNewCell", + "interactiveWindow.promptToSaveOnClose", + "interactiveWindow", "interactive.open", "interactive.execute", "interactive.input.clear", "interactive.history.previous", "interactive.history.next", - "interactiveScrollToTop", - "interactiveScrollToBottom", "interactive.input.focus", - "interactive.history.focus", - "interactive.activeCodeBorder", - "interactive.inactiveCodeBorder", - "interactiveWindow.alwaysScrollOnNewCell" + "interactive.history.focus" ], "vs/workbench/contrib/quickaccess/browser/quickAccess.contribution": [ "helpQuickAccessPlaceholder", @@ -3235,18 +3469,78 @@ "commandPalette", "commandPalette" ], + "vs/workbench/contrib/testing/browser/testing.contribution": [ + { + "key": "miViewTesting", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "noTestProvidersRegistered", + "searchForAdditionalTestExtensions", + "test", + "testResultsPanelName", + "testResultsPanelName", + "testExplorer", + "testCoverage" + ], + "vs/workbench/contrib/files/browser/explorerViewlet": [ + "explorerViewIcon", + "openEditorsIcon", + { + "key": "miViewExplorer", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "openFolder", + "addAFolder", + "openRecent", + { + "key": "noWorkspaceHelp", + "comment": [ + "Please do not translate the word \"command\", it is part of our internal syntax which must not change" + ] + }, + { + "key": "noFolderHelpWeb", + "comment": [ + "Please do not translate the word \"command\", it is part of our internal syntax which must not change" + ] + }, + { + "key": "remoteNoFolderHelp", + "comment": [ + "Please do not translate the word \"command\", it is part of our internal syntax which must not change" + ] + }, + { + "key": "noFolderButEditorsHelp", + "comment": [ + "Please do not translate the word \"command\", it is part of our internal syntax which must not change" + ] + }, + { + "key": "noFolderHelp", + "comment": [ + "Please do not translate the word \"command\", it is part of our internal syntax which must not change" + ] + }, + "folders", + "explore", + "explore" + ], + "vs/workbench/contrib/logs/common/logs.contribution": [ + "remote name", + "setDefaultLogLevel", + "show window log" + ], "vs/workbench/contrib/files/browser/fileActions.contribution": [ "copyPath", "copyRelativePath", "revealInSideBar", "acceptLocalChanges", "revertLocalChanges", - "copyPathOfActive", - "copyRelativePathOfActive", - "saveAllInGroup", - "saveFiles", - "revert", - "compareActiveWithSaved", "openToSide", "reopenWith", "revert", @@ -3312,53 +3606,55 @@ "comment": [ "&& denotes a mnemonic" ] - } + }, + "copyPathOfActive", + "copyRelativePathOfActive", + "saveAllInGroup", + "saveFiles", + "revert", + "compareActiveWithSaved", + "compareActiveWithSavedMeta", + "newFolderDescription" ], - "vs/workbench/contrib/files/browser/explorerViewlet": [ - "explorerViewIcon", - "openEditorsIcon", - "folders", - "explore", - "explore", + "vs/workbench/contrib/bulkEdit/browser/bulkEditService": [ + "summary.0", + "summary.nm", + "summary.n0", + "summary.textFiles", + "workspaceEdit", + "workspaceEdit", + "nothing", + "closeTheWindow.message", { - "key": "miViewExplorer", + "key": "closeTheWindow", "comment": [ "&& denotes a mnemonic" ] }, - "openFolder", - "addAFolder", - "openRecent", - { - "key": "noWorkspaceHelp", - "comment": [ - "Please do not translate the word \"commmand\", it is part of our internal syntax which must not change" - ] - }, + "changeWorkspace.message", { - "key": "noFolderHelpWeb", + "key": "changeWorkspace", "comment": [ - "Please do not translate the word \"commmand\", it is part of our internal syntax which must not change" + "&& denotes a mnemonic" ] }, + "reloadTheWindow.message", { - "key": "remoteNoFolderHelp", + "key": "reloadTheWindow", "comment": [ - "Please do not translate the word \"commmand\", it is part of our internal syntax which must not change" + "&& denotes a mnemonic" ] }, + "quit.message", { - "key": "noFolderButEditorsHelp", + "key": "quit", "comment": [ - "Please do not translate the word \"commmand\", it is part of our internal syntax which must not change" + "&& denotes a mnemonic" ] }, - { - "key": "noFolderHelp", - "comment": [ - "Please do not translate the word \"commmand\", it is part of our internal syntax which must not change" - ] - } + "areYouSureQuiteBulkEdit.detail", + "fileOperation", + "refactoring.autoSave" ], "vs/workbench/contrib/files/browser/files.contribution": [ "textFileEditor", @@ -3390,6 +3686,7 @@ "eol", "useTrash", "trimTrailingWhitespace", + "trimTrailingWhitespaceInRegexAndStrings", "insertFinalNewline", "trimFinalNewlines", { @@ -3428,6 +3725,18 @@ ], "key": "autoSaveDelay" }, + { + "comment": [ + "This is the description for a setting. Values surrounded by single quotes are not to be translated." + ], + "key": "autoSaveWorkspaceFilesOnly" + }, + { + "comment": [ + "This is the description for a setting. Values surrounded by single quotes are not to be translated." + ], + "key": "autoSaveWhenNoErrors" + }, "watcherExclude", "watcherInclude", "defaultLanguage", @@ -3493,6 +3802,7 @@ "explorer.autoRevealExclude.when", "enableDragAndDrop", "confirmDragAndDrop", + "confirmPasteNative", "confirmDelete", "enableUndo", "confirmUndo", @@ -3529,46 +3839,6 @@ "fileNestingPatterns", "fileNesting.description" ], - "vs/workbench/contrib/bulkEdit/browser/bulkEditService": [ - "summary.0", - "summary.nm", - "summary.n0", - "summary.textFiles", - "workspaceEdit", - "workspaceEdit", - "nothing", - "closeTheWindow.message", - { - "key": "closeTheWindow", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "changeWorkspace.message", - { - "key": "changeWorkspace", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "reloadTheWindow.message", - { - "key": "reloadTheWindow", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "quit.message", - { - "key": "quit", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "areYouSureQuiteBulkEdit.detail", - "fileOperation", - "refactoring.autoSave" - ], "vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution": [ "overlap", "detail", @@ -3578,6 +3848,7 @@ "&& denotes a mnemonic" ] }, + "refactorPreviewViewIcon", "apply", "cat", "Discard", @@ -3590,35 +3861,10 @@ "cat", "groupByType", "cat", - "refactorPreviewViewIcon", "panel", "panel" ], - "vs/workbench/contrib/searchEditor/browser/searchEditor.contribution": [ - "searchEditor", - "promptOpenWith.searchEditor.displayName", - "search", - "searchEditor.deleteResultBlock", - "search.openNewSearchEditor", - "search.openSearchEditor", - "search.openNewEditorToSide", - "search.openResultsInEditor", - "search.rerunSearchInEditor", - "search.action.focusQueryEditorWidget", - "search.action.focusFilesToInclude", - "search.action.focusFilesToExclude", - "searchEditor.action.toggleSearchEditorCaseSensitive", - "searchEditor.action.toggleSearchEditorWholeWord", - "searchEditor.action.toggleSearchEditorRegex", - "searchEditor.action.toggleSearchEditorContextLines", - "searchEditor.action.increaseSearchEditorContextLines", - "searchEditor.action.decreaseSearchEditorContextLines", - "searchEditor.action.selectAllSearchEditorMatches", - "search.openNewEditor" - ], "vs/workbench/contrib/search/browser/search.contribution": [ - "name", - "search", { "key": "miViewSearch", "comment": [ @@ -3679,6 +3925,9 @@ "search.searchEditor.doubleClickBehaviour.goToLocation", "search.searchEditor.doubleClickBehaviour.openLocationToSide", "search.searchEditor.doubleClickBehaviour", + "search.searchEditor.singleClickBehaviour.default", + "search.searchEditor.singleClickBehaviour.peekDefinition", + "search.searchEditor.singleClickBehaviour", { "key": "search.searchEditor.reusePriorSearchConfiguration", "comment": [ @@ -3698,8 +3947,10 @@ "scm.defaultViewMode.tree", "scm.defaultViewMode.list", "search.defaultViewMode", + "search.quickAccess.preserveInput", "search.experimental.closedNotebookResults", - "search.experimental.quickAccess.preserveInput" + "search", + "search" ], "vs/workbench/contrib/search/browser/searchView": [ "searchCanceled", @@ -3732,7 +3983,6 @@ "removeAll.occurrences.files.confirmation.message", "replaceAll.occurrences.files.confirmation.message", "emptySearch", - "ariaSearchResultsClearStatus", "searchPathNotFoundError", "noOpenEditorResultsIncludesExcludes", "noOpenEditorResultsIncludes", @@ -3764,50 +4014,145 @@ "searchWithoutFolder", "openFolder" ], + "vs/workbench/contrib/searchEditor/browser/searchEditor.contribution": [ + "searchEditor", + "promptOpenWith.searchEditor.displayName", + "search.openNewEditor", + "search", + "searchEditor.deleteResultBlock", + "search.openNewSearchEditor", + "search.openSearchEditor", + "search.openNewEditorToSide", + "search.openResultsInEditor", + "search.rerunSearchInEditor", + "search.action.focusQueryEditorWidget", + "search.action.focusFilesToInclude", + "search.action.focusFilesToExclude", + "searchEditor.action.toggleSearchEditorCaseSensitive", + "searchEditor.action.toggleSearchEditorWholeWord", + "searchEditor.action.toggleSearchEditorRegex", + "searchEditor.action.toggleSearchEditorContextLines", + "searchEditor.action.increaseSearchEditorContextLines", + "searchEditor.action.decreaseSearchEditorContextLines", + "searchEditor.action.selectAllSearchEditorMatches" + ], "vs/workbench/contrib/sash/browser/sash.contribution": [ "sashSize", "sashHoverDelay" ], - "vs/workbench/contrib/debug/browser/debug.contribution": [ - "debugCategory", - "startDebugPlaceholder", - "startDebuggingHelp", - "tasksQuickAccessPlaceholder", - "tasksQuickAccessHelp", - "terminateThread", - { - "comment": [ - "Debug is a noun in this context, not a verb." - ], - "key": "debugFocusConsole" - }, - "jumpToCursor", - "SetNextStatement", - "inlineBreakpoint", - "terminateThread", - "restartFrame", - "copyStackTrace", - "viewMemory", - "setValue", - "copyValue", - "copyAsExpression", - "addToWatchExpressions", - "breakWhenValueIsRead", - "breakWhenValueChanges", - "breakWhenValueIsAccessed", - "editWatchExpression", - "setValue", - "copyValue", - "viewMemory", - "removeWatchExpression", - "run", + "vs/workbench/contrib/scm/browser/scm.contribution": [ + "sourceControlViewIcon", + "no open repo", + "no open repo in an untrusted workspace", + "manageWorkspaceTrustAction", { - "key": "mRun", + "key": "miViewSCM", "comment": [ "&& denotes a mnemonic" ] }, - { + "scmConfigurationTitle", + "scm.diffDecorations.all", + "scm.diffDecorations.gutter", + "scm.diffDecorations.overviewRuler", + "scm.diffDecorations.minimap", + "scm.diffDecorations.none", + "diffDecorations", + "diffGutterWidth", + "scm.diffDecorationsGutterVisibility.always", + "scm.diffDecorationsGutterVisibility.hover", + "scm.diffDecorationsGutterVisibility", + "scm.diffDecorationsGutterAction.diff", + "scm.diffDecorationsGutterAction.none", + "scm.diffDecorationsGutterAction", + "diffGutterPattern", + "diffGutterPatternAdded", + "diffGutterPatternModifed", + "scm.diffDecorationsIgnoreTrimWhitespace.true", + "scm.diffDecorationsIgnoreTrimWhitespace.false", + "scm.diffDecorationsIgnoreTrimWhitespace.inherit", + "diffDecorationsIgnoreTrimWhitespace", + "alwaysShowActions", + "scm.countBadge.all", + "scm.countBadge.focused", + "scm.countBadge.off", + "scm.countBadge", + "scm.providerCountBadge.hidden", + "scm.providerCountBadge.auto", + "scm.providerCountBadge.visible", + "scm.providerCountBadge", + "scm.defaultViewMode.tree", + "scm.defaultViewMode.list", + "scm.defaultViewMode", + "scm.defaultViewSortKey.name", + "scm.defaultViewSortKey.path", + "scm.defaultViewSortKey.status", + "scm.defaultViewSortKey", + "autoReveal", + "inputFontFamily", + "inputFontSize", + "inputMaxLines", + "inputMinLines", + "alwaysShowRepository", + "scm.repositoriesSortOrder.discoveryTime", + "scm.repositoriesSortOrder.name", + "scm.repositoriesSortOrder.path", + "repositoriesSortOrder", + "providersVisible", + "showActionButton", + "showInputActionButton", + "scm.showIncomingChanges.always", + "scm.showIncomingChanges.never", + "scm.showIncomingChanges.auto", + "scm.showIncomingChanges", + "scm.showOutgoingChanges.always", + "scm.showOutgoingChanges.never", + "scm.showOutgoingChanges.auto", + "scm.showOutgoingChanges", + "scm.showChangesSummary", + "scm.workingSets.enabled", + "scm.workingSets.default.empty", + "scm.workingSets.default.current", + "scm.workingSets.default", + "scm accept", + "scm view next commit", + "scm view previous commit", + "open in external terminal", + "open in integrated terminal", + "source control", + "source control", + "source control repositories" + ], + "vs/workbench/contrib/debug/browser/debug.contribution": [ + "debugCategory", + "startDebugPlaceholder", + "startDebuggingHelp", + "tasksQuickAccessPlaceholder", + "tasksQuickAccessHelp", + "terminateThread", + "restartFrame", + "copyStackTrace", + "viewMemory", + "setValue", + "breakWhenValueIsRead", + "breakWhenValueChanges", + "breakWhenValueIsAccessed", + "viewMemory", + "breakWhenValueIsRead", + "breakWhenValueChanges", + "breakWhenValueIsAccessed", + "editWatchExpression", + "setValue", + "copyValue", + "viewMemory", + "removeWatchExpression", + { + "key": "mRun", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { "key": "miStartDebugging", "comment": [ "&& denotes a mnemonic" @@ -3879,36 +4224,18 @@ "&& denotes a mnemonic" ] }, - { - "comment": [ - "Debug is a noun in this context, not a verb." - ], - "key": "debugPanel" - }, - { - "comment": [ - "Debug is a noun in this context, not a verb." - ], - "key": "debugPanel" - }, { "key": "miToggleDebugConsole", "comment": [ "&& denotes a mnemonic" ] }, - "run and debug", { "key": "miViewRun", "comment": [ "&& denotes a mnemonic" ] }, - "variables", - "watch", - "callStack", - "breakpoints", - "loadedScripts", "disassembly", "debugConfigurationTitle", { @@ -3917,12 +4244,28 @@ ], "key": "allowBreakpointsEverywhere" }, + { + "comment": [ + "This is the description for a setting" + ], + "key": "gutterMiddleClickAction" + }, + "debug.gutterMiddleClickAction.logpoint", + "debug.gutterMiddleClickAction.conditionalBreakpoint", + "debug.gutterMiddleClickAction.triggeredBreakpoint", + "debug.gutterMiddleClickAction.none", { "comment": [ "This is the description for a setting" ], "key": "openExplorerOnEnd" }, + { + "comment": [ + "This is the description for a setting" + ], + "key": "closeReadonlyTabsOnEnd" + }, { "comment": [ "This is the description for a setting" @@ -4006,9 +4349,54 @@ "debug.confirmOnExit.always", "debug.disassemblyView.showSourceCode", "debug.autoExpandLazyVariables", - "debug.enableStatusBarColor" + "debug.enableStatusBarColor", + { + "comment": [ + "This is the description for a setting" + ], + "key": "debug.hideLauncherWhileDebugging" + }, + "terminateThread", + { + "comment": [ + "Debug is a noun in this context, not a verb." + ], + "key": "debugFocusConsole" + }, + "jumpToCursor", + "SetNextStatement", + "inlineBreakpoint", + "run", + "runMenu", + { + "comment": [ + "Debug is a noun in this context, not a verb." + ], + "key": "debugPanel" + }, + { + "comment": [ + "Debug is a noun in this context, not a verb." + ], + "key": "debugPanel" + }, + "run and debug", + "variables", + "watch", + "callStack", + "breakpoints", + "loadedScripts" + ], + "vs/workbench/contrib/debug/browser/callStackEditorContribution": [ + "topStackFrameLineHighlight", + "focusedStackFrameLineHighlight" + ], + "vs/workbench/contrib/debug/browser/debugEditorContribution": [ + "editor.inlineValuesForeground", + "editor.inlineValuesBackground" ], "vs/workbench/contrib/debug/browser/breakpointEditorContribution": [ + "breakpointHelper", "logPoint", "breakpoint", "breakpointHasConditionDisabled", @@ -4056,6 +4444,7 @@ "addBreakpoint", "addConditionalBreakpoint", "addLogPoint", + "addTriggeredBreakpoint", "runToLine", "debugIcon.breakpointForeground", "debugIcon.breakpointDisabledForeground", @@ -4063,109 +4452,6 @@ "debugIcon.breakpointCurrentStackframeForeground", "debugIcon.breakpointStackframeForeground" ], - "vs/workbench/contrib/scm/browser/scm.contribution": [ - "sourceControlViewIcon", - "source control", - "no open repo", - "no open repo in an untrusted workspace", - "manageWorkspaceTrustAction", - "source control", - { - "key": "miViewSCM", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "source control repositories", - "source control sync", - "scmConfigurationTitle", - "scm.diffDecorations.all", - "scm.diffDecorations.gutter", - "scm.diffDecorations.overviewRuler", - "scm.diffDecorations.minimap", - "scm.diffDecorations.none", - "diffDecorations", - "diffGutterWidth", - "scm.diffDecorationsGutterVisibility.always", - "scm.diffDecorationsGutterVisibility.hover", - "scm.diffDecorationsGutterVisibility", - "scm.diffDecorationsGutterAction.diff", - "scm.diffDecorationsGutterAction.none", - "scm.diffDecorationsGutterAction", - "diffGutterPattern", - "diffGutterPatternAdded", - "diffGutterPatternModifed", - "scm.diffDecorationsIgnoreTrimWhitespace.true", - "scm.diffDecorationsIgnoreTrimWhitespace.false", - "scm.diffDecorationsIgnoreTrimWhitespace.inherit", - "diffDecorationsIgnoreTrimWhitespace", - "alwaysShowActions", - "scm.countBadge.all", - "scm.countBadge.focused", - "scm.countBadge.off", - "scm.countBadge", - "scm.providerCountBadge.hidden", - "scm.providerCountBadge.auto", - "scm.providerCountBadge.visible", - "scm.providerCountBadge", - "scm.defaultViewMode.tree", - "scm.defaultViewMode.list", - "scm.defaultViewMode", - "scm.defaultViewSortKey.name", - "scm.defaultViewSortKey.path", - "scm.defaultViewSortKey.status", - "scm.defaultViewSortKey", - "autoReveal", - "inputFontFamily", - "inputFontSize", - "alwaysShowRepository", - "scm.repositoriesSortOrder.discoveryTime", - "scm.repositoriesSortOrder.name", - "scm.repositoriesSortOrder.path", - "repositoriesSortOrder", - "providersVisible", - "showActionButton", - "showSyncView", - "scm accept", - "scm view next commit", - "scm view previous commit", - "open in external terminal", - "open in integrated terminal" - ], - "vs/workbench/contrib/debug/browser/debugEditorContribution": [ - "editor.inlineValuesForeground", - "editor.inlineValuesBackground" - ], - "vs/workbench/contrib/debug/browser/repl": [ - { - "key": "workbench.debug.filter.placeholder", - "comment": [ - "Text in the brackets after e.g. is not localizable" - ] - }, - "showing filtered repl lines", - "debugConsole", - "startDebugFirst", - { - "key": "actions.repl.acceptInput", - "comment": [ - "Apply input from the debug console input box" - ] - }, - "repl.action.filter", - "actions.repl.copyAll", - "selectRepl", - "clearRepl", - "debugConsoleCleared", - "collapse", - "paste", - "copyAll", - "copy" - ], - "vs/workbench/contrib/debug/browser/callStackEditorContribution": [ - "topStackFrameLineHighlight", - "focusedStackFrameLineHighlight" - ], "vs/workbench/contrib/markers/browser/markers.contribution": [ "markersViewIcon", { @@ -4176,41 +4462,38 @@ }, "viewAsTree", "viewAsTable", - "toggle errors", + "show errors", "problems", - "errors", - "toggle warnings", + "show warnings", "problems", - "warnings", - "toggle infos", + "show infos", "problems", - "Infos", - "toggle active file", + "show active file", "problems", - "Active File", - "toggle Excluded Files", + "show excluded files", "problems", - "Excluded Files", - "copyMarker", - "copyMessage", - "copyMessage", "focusProblemsList", "focusProblemsFilter", - "show multiline", "problems", - "show singleline", "problems", "clearFiltersText", "problems", "collapseAll", "status.problems", + "status.problemsVisibilityOff", + "status.problemsVisibility", "totalErrors", "totalWarnings", "totalInfos", "noProblems", "manyProblems", - "totalProblems" - ], + "totalProblems", + "copyMarker", + "copyMessage", + "copyMessage", + "show multiline", + "show singleline" + ], "vs/workbench/contrib/debug/browser/debugViewlet": [ { "key": "miOpenConfigurations", @@ -4225,21 +4508,13 @@ ] }, "debugPanel", - "startAdditionalSession" - ], - "vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution": [ - "name", - "diffAlgorithm.legacy", - "diffAlgorithm.advanced" - ], - "vs/workbench/contrib/commands/common/commands.contribution": [ - "runCommands", - "runCommands.description", - "runCommands.commands", - "runCommands.invalidArgs", - "runCommands.noCommandsToRun" + "startAdditionalSession", + "openLaunchConfigDescription" ], "vs/workbench/contrib/comments/browser/comments.contribution": [ + "collapseAll", + "expandAll", + "reply", "commentsConfigurationTitle", "openComments", "comments.openPanel.deprecated", @@ -4252,28 +4527,24 @@ "comments.visible", "comments.maxHeight", "collapseOnResolve", - "intro", - "introWidget", - "introWidgetNoKb", - "commentCommands", - "escape", - "next", - "nextNoKb", - "previous", - "previousNoKb", - "nextCommentThreadKb", - "nextCommentThreadNoKb", - "previousCommentThreadKb", - "previousCommentThreadNoKb", - "addComment", - "addCommentNoKb", - "submitComment", - "submitCommentNoKb" + "totalUnresolvedComments" + ], + "vs/workbench/contrib/commands/common/commands.contribution": [ + "runCommands.description", + "runCommands.commands", + "runCommands.invalidArgs", + "runCommands.noCommandsToRun", + "runCommands" + ], + "vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution": [ + "name", + "diffAlgorithm.legacy", + "diffAlgorithm.advanced" ], "vs/workbench/contrib/url/browser/url.contribution": [ - "openUrl", "urlToOpen", - "workbench.trustedDomains.promptInTrustedWorkspace" + "workbench.trustedDomains.promptInTrustedWorkspace", + "openUrl" ], "vs/workbench/contrib/webview/browser/webview.contribution": [ "cut", @@ -4283,23 +4554,107 @@ "vs/workbench/contrib/webviewPanel/browser/webviewPanel.contribution": [ "webview.editor.label" ], + "vs/workbench/contrib/extensions/browser/extensionsViewlet": [ + "installed", + "searchExtensions", + "extensionFoundInSection", + "extensionFound", + "extensionsFoundInSection", + "extensionsFound", + "suggestProxyError", + "open user settings", + "extensionToUpdate", + "extensionsToUpdate", + "extensionToReload", + "extensionsToReload", + "malicious warning", + "reloadNow", + { + "key": "remote", + "comment": [ + "Remote as in remote machine" + ] + }, + "select and install local extensions", + "install remote in local", + "popularExtensions", + "recommendedExtensions", + "enabledExtensions", + "disabledExtensions", + "marketPlace", + "installed", + "recently updated", + "enabled", + "disabled", + "availableUpdates", + "builtin", + "workspaceUnsupported", + "workspaceRecommendedExtensions", + "otherRecommendedExtensions", + "builtinFeatureExtensions", + "builtInThemesExtensions", + "builtinProgrammingLanguageExtensions", + "untrustedUnsupportedExtensions", + "untrustedPartiallySupportedExtensions", + "virtualUnsupportedExtensions", + "virtualPartiallySupportedExtensions", + "deprecated" + ], + "vs/workbench/contrib/output/browser/outputView": [ + "output model title", + "channel", + "output", + "outputViewAriaLabel" + ], + "vs/workbench/contrib/output/browser/output.contribution": [ + "outputViewIcon", + { + "key": "miToggleOutput", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "switchBetweenOutputs.label", + "switchToOutput.label", + "selectOutput", + "outputScrollOff", + "outputScrollOn", + "logLevel.label", + "logLevelDefault.label", + "extensionLogs", + "selectlog", + "logFile", + "selectlogFile", + "output", + "output.smartScroll.enabled", + "output", + "output", + "showOutputChannels", + "output", + "clearOutput.label", + "toggleAutoScroll", + "openActiveOutputFile", + "openActiveOutputFileInNewWindow", + "showLogs", + "openLogFile" + ], "vs/workbench/contrib/extensions/browser/extensions.contribution": [ "manageExtensionsQuickAccessPlaceholder", "manageExtensionsHelp", "extension", - "extensions", { "key": "miViewExtensions", "comment": [ "&& denotes a mnemonic" ] }, - "extensionsConfigurationTitle", "all", "enabled", + "selected", "none", "extensions.autoUpdate.true", "extensions.autoUpdate.enabled", + "extensions.autoUpdate.selected", "extensions.autoUpdate.false", "extensions.autoUpdate", "extensionsCheckUpdates", @@ -4320,12 +4675,15 @@ "extensions.supportUntrustedWorkspaces.supported", "extensions.supportUntrustedWorkspaces.version", "extensionsDeferredStartupFinishedActivation", + "extensionsInQuickAccess", "notFound", "workbench.extensions.installExtension.description", "workbench.extensions.installExtension.arg.decription", "workbench.extensions.installExtension.option.installOnlyNewlyAddedFromExtensionPackVSIX", "workbench.extensions.installExtension.option.installPreReleaseVersion", "workbench.extensions.installExtension.option.donotSync", + "workbench.extensions.installExtension.option.justification", + "workbench.extensions.installExtension.option.enable", "workbench.extensions.installExtension.option.context", "notFound", "workbench.extensions.uninstallExtension.description", @@ -4344,25 +4702,13 @@ ] }, "showExtensions", - "focusExtensions", - "installExtensions", - "showRecommendedKeymapExtensionsShort", "importKeyboardShortcutsFroms", - "showLanguageExtensionsShort", - "checkForUpdates", "noUpdatesAvailable", "configure auto updating extensions", "configureExtensionsAutoUpdate.all", "configureExtensionsAutoUpdate.enabled", + "configureExtensionsAutoUpdate.selected", "configureExtensionsAutoUpdate.none", - "updateAll", - "disableAutoUpdate", - "enableAutoUpdate", - "enableAll", - "enableAllWorkspace", - "disableAll", - "disableAllWorkspace", - "InstallFromVSIX", "installFromVSIX", { "key": "installButton", @@ -4374,30 +4720,20 @@ "InstallVSIXAction.successReload", "InstallVSIXAction.success", "InstallVSIXAction.reloadNow", - "installExtensionFromLocation", "installFromLocation", "install button", "installFromLocationPlaceHolder", "installFromLocation", "filterExtensions", - "showFeaturedExtensions", "featured filter", - "showPopularExtensions", "most popular filter", - "showRecommendedExtensions", "most popular recommended", - "recentlyPublishedExtensions", "recently published filter", "filter by category", - "showBuiltInExtensions", "builtin filter", - "extensionUpdates", "extension updates filter", - "showWorkspaceUnsupportedExtensions", "workspace unsupported filter", - "showEnabledExtensions", "enabled filter", - "showDisabledExtensions", "disabled filter", "sorty by", "sort by installs", @@ -4405,18 +4741,49 @@ "sort by name", "sort by published date", "sort by update date", - "clearExtensionsSearchResults", - "refreshExtension", "installWorkspaceRecommendedExtensions", - "show pre-release version", - "show released version", - "workbench.extensions.action.copyExtension", + "enablePreRleaseLabel", + "disablePreRleaseLabel", "extensionInfoName", "extensionInfoId", "extensionInfoDescription", "extensionInfoVersion", "extensionInfoPublisher", "extensionInfoVSMarketplaceLink", + "extensions", + "extensions", + "extensions", + "extensions", + "extensions", + "extensions", + "focusExtensions", + "installExtensions", + "showRecommendedKeymapExtensionsShort", + "showLanguageExtensionsShort", + "checkForUpdates", + "updateAll", + "disableAutoUpdate", + "enableAutoUpdate", + "enableAll", + "enableAllWorkspace", + "disableAll", + "disableAllWorkspace", + "InstallFromVSIX", + "installExtensionFromLocation", + "showFeaturedExtensions", + "showPopularExtensions", + "showRecommendedExtensions", + "recentlyPublishedExtensions", + "showBuiltInExtensions", + "extensionUpdates", + "showWorkspaceUnsupportedExtensions", + "showEnabledExtensions", + "showDisabledExtensions", + "clearExtensionsSearchResults", + "refreshExtension", + "show pre-release version", + "show released version", + "workbench.extensions.action.copyExtension", "workbench.extensions.action.copyExtensionId", "workbench.extensions.action.configure", "workbench.extensions.action.configureKeybindings", @@ -4427,115 +4794,28 @@ "workbench.extensions.action.addExtensionToWorkspaceRecommendations", "workbench.extensions.action.removeExtensionFromWorkspaceRecommendations", "workbench.extensions.action.addToWorkspaceRecommendations", - "extensions", "workbench.extensions.action.addToWorkspaceFolderRecommendations", - "extensions", "workbench.extensions.action.addToWorkspaceIgnoredRecommendations", - "extensions", - "workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations", - "extensions", - "extensions" + "workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations" ], - "vs/workbench/contrib/extensions/browser/extensionsViewlet": [ + "vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution": [ + "scopedConsoleAction.Integrated", + "scopedConsoleAction.external", + "scopedConsoleAction.wt" + ], + "vs/workbench/contrib/relauncher/browser/relauncher.contribution": [ + "relaunchSettingMessage", + "relaunchSettingMessageWeb", + "relaunchSettingDetail", + "relaunchSettingDetailWeb", { - "key": "remote", + "key": "restart", "comment": [ - "Remote as in remote machine" + "&& denotes a mnemonic" ] }, - "installed", - "select and install local extensions", - "install remote in local", - "popularExtensions", - "recommendedExtensions", - "enabledExtensions", - "disabledExtensions", - "marketPlace", - "installed", - "recently updated", - "enabled", - "disabled", - "availableUpdates", - "builtin", - "workspaceUnsupported", - "workspaceRecommendedExtensions", - "otherRecommendedExtensions", - "builtinFeatureExtensions", - "builtInThemesExtensions", - "builtinProgrammingLanguageExtensions", - "untrustedUnsupportedExtensions", - "untrustedPartiallySupportedExtensions", - "virtualUnsupportedExtensions", - "virtualPartiallySupportedExtensions", - "deprecated", - "searchExtensions", - "extensionFoundInSection", - "extensionFound", - "extensionsFoundInSection", - "extensionsFound", - "suggestProxyError", - "open user settings", - "extensionToUpdate", - "extensionsToUpdate", - "extensionToReload", - "extensionsToReload", - "malicious warning", - "reloadNow" - ], - "vs/workbench/contrib/output/browser/output.contribution": [ - "outputViewIcon", - "output", - "output", { - "key": "miToggleOutput", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "switchBetweenOutputs.label", - "switchToOutput.label", - "showOutputChannels", - "output", - "selectOutput", - "clearOutput.label", - "outputCleared", - "toggleAutoScroll", - "outputScrollOff", - "outputScrollOn", - "openActiveLogOutputFile", - "showLogs", - "extensionLogs", - "selectlog", - "openLogFile", - "logFile", - "selectlogFile", - "output", - "output.smartScroll.enabled" - ], - "vs/workbench/contrib/output/browser/outputView": [ - "output model title", - "channel", - "output", - "outputViewAriaLabel" - ], - "vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution": [ - "scopedConsoleAction.Integrated", - "scopedConsoleAction.external", - "scopedConsoleAction.wt" - ], - "vs/workbench/contrib/relauncher/browser/relauncher.contribution": [ - "relaunchSettingMessage", - "relaunchSettingMessageWeb", - "relaunchSettingDetail", - "relaunchSettingDetailWeb", - { - "key": "restart", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "restartWeb", + "key": "restartWeb", "comment": [ "&& denotes a mnemonic" ] @@ -4589,18 +4869,6 @@ "&& denotes a mnemonic" ] }, - "workbench.action.tasks.openWorkspaceFileTasks", - "ShowLogAction.label", - "RunTaskAction.label", - "ReRunTaskAction.label", - "RestartTaskAction.label", - "ShowTasksAction.label", - "TerminateAction.label", - "BuildAction.label", - "TestAction.label", - "ConfigureDefaultBuildTask.label", - "ConfigureDefaultTestTask.label", - "workbench.action.tasks.openUserTasks", "userTasks", "tasksQuickAccessPlaceholder", "tasksQuickAccessHelp", @@ -4623,7 +4891,20 @@ "task.saveBeforeRun", "task.saveBeforeRun.always", "task.saveBeforeRun.never", - "task.SaveBeforeRun.prompt" + "task.SaveBeforeRun.prompt", + "task.verboseLogging", + "workbench.action.tasks.openWorkspaceFileTasks", + "ShowLogAction.label", + "RunTaskAction.label", + "ReRunTaskAction.label", + "RestartTaskAction.label", + "ShowTasksAction.label", + "TerminateAction.label", + "BuildAction.label", + "TestAction.label", + "ConfigureDefaultBuildTask.label", + "ConfigureDefaultTestTask.label", + "workbench.action.tasks.openUserTasks" ], "vs/workbench/contrib/remote/common/remote.contribution": [ "invalidWorkspaceMessage", @@ -4634,8 +4915,6 @@ "&& denotes a mnemonic" ] }, - "triggerReconnect", - "pauseSocketWriting", "ui", "workspace", "remote", @@ -4646,6 +4925,7 @@ "remote.autoForwardPortsSource.process", "remote.autoForwardPortsSource.output", "remote.autoForwardPortsSource.hybrid", + "remote.autoForwardPortFallback", "remote.forwardOnClick", "remote.portsAttributes.port", "remote.portsAttributes.notify", @@ -4675,7 +4955,38 @@ "remote.portsAttributes.requireLocalPort", "remote.portsAttributes.protocol", "remote.portsAttributes.defaults", - "remote.localPortHost" + "remote.localPortHost", + "triggerReconnect", + "pauseSocketWriting" + ], + "vs/workbench/contrib/debug/browser/repl": [ + { + "key": "workbench.debug.filter.placeholder", + "comment": [ + "Text in the brackets after e.g. is not localizable" + ] + }, + "showing filtered repl lines", + "debugConsole", + "startDebugFirst", + { + "key": "actions.repl.acceptInput", + "comment": [ + "Apply input from the debug console input box" + ] + }, + "repl.action.filter", + "actions.repl.copyAll", + "selectRepl", + "collapse", + "paste", + "copyAll", + "copy", + "clearRepl", + "clearRepl.descriotion" + ], + "vs/workbench/contrib/keybindings/browser/keybindings.contribution": [ + "toggleKeybindingsLog" ], "vs/workbench/contrib/snippets/browser/snippets.contribution": [ "editor.snippets.codeActions.enabled", @@ -4694,9 +5005,6 @@ "nullFormatterDescription", "formatter.default" ], - "vs/workbench/contrib/keybindings/browser/keybindings.contribution": [ - "toggleKeybindingsLog" - ], "vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution": [ "status.button.configure", "colorDecoratorsStatusItem.name", @@ -4713,29 +5021,6 @@ "read.title", "stop.title" ], - "vs/workbench/contrib/update/browser/update.contribution": [ - "showReleaseNotes", - { - "key": "mshowReleaseNotes", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "update.noReleaseNotesOnline", - "checkForUpdates", - "downloadUpdate", - "installUpdate", - "restartToUpdate", - "openDownloadPage", - "applyUpdate", - "pickUpdate", - { - "key": "updateButton", - "comment": [ - "&& denotes a mnemonic" - ] - } - ], "vs/workbench/contrib/themes/browser/themes.contribution": [ "manageExtensionIcon", "themes.selectMarketplaceTheme", @@ -4743,29 +5028,36 @@ "installExtension.confirm", "installExtension.button.ok", "installing extensions", - "selectTheme.label", + "themes.selectTheme.darkScheme", + "themes.selectTheme.lightScheme", + "themes.selectTheme.darkHC", + "themes.selectTheme.lightHC", + "themes.selectTheme.default", + "themes.configure.switchingEnabled", + "themes.configure.switchingDisabled", "installColorThemes", "browseColorThemes", - "themes.selectTheme", "themes.category.light", "themes.category.dark", "themes.category.hc", - "selectIconTheme.label", "installIconThemes", "themes.selectIconTheme", "fileIconThemeCategory", "noIconThemeLabel", "noIconThemeDesc", - "selectProductIconTheme.label", "installProductIconThemes", "browseProductIconThemes", "themes.selectProductIconTheme", "productIconThemeCategory", "defaultProductIconThemeLabel", "manage extension", - "generateColorTheme.label", - "toggleLightDarkThemes.label", - "browseColorThemeInMarketPlace.label", + { + "key": "cannotToggle", + "comment": [ + "{0} is a setting name" + ] + }, + "goToSetting", "themes", { "key": "miSelectTheme", @@ -4792,7 +5084,45 @@ "comment": [ "{0} is the name of the new default theme" ] - } + }, + "selectTheme.label", + "selectIconTheme.label", + "selectProductIconTheme.label", + "generateColorTheme.label", + "toggleLightDarkThemes.label", + "browseColorThemeInMarketPlace.label" + ], + "vs/workbench/contrib/update/browser/update.contribution": [ + { + "key": "mshowReleaseNotes", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "update.noReleaseNotesOnline", + { + "key": "mshowReleaseNotes", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "releaseNotesFromFileNone", + "pickUpdate", + { + "key": "updateButton", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "showReleaseNotes", + "showReleaseNotesCurrentFile", + "developerCategory", + "checkForUpdates", + "downloadUpdate", + "installUpdate", + "restartToUpdate", + "openDownloadPage", + "applyUpdate" ], "vs/workbench/contrib/surveys/browser/nps.contribution": [ "surveyQuestion", @@ -4800,25 +5130,21 @@ "remindLater", "neverAgain" ], + "vs/workbench/contrib/surveys/browser/ces.contribution": [ + "cesSurveyQuestion", + "giveFeedback", + "remindLater" + ], "vs/workbench/contrib/surveys/browser/languageSurveys.contribution": [ "helpUs", "takeShortSurvey", "remindLater", "neverAgain" ], - "vs/workbench/contrib/surveys/browser/ces.contribution": [ - "cesSurveyQuestion", - "giveFeedback", - "remindLater" - ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution": [ - "miWelcome", "welcome", - "welcome", - "welcome.goBack", "welcome.markStepComplete", "welcome.markStepInomplete", - "welcome.showAllWalkthroughs", "pickWalkthroughs", "workspacePlatform", "workbench.welcomePage.walkthroughs.openOnInstall", @@ -4852,9 +5178,20 @@ ], "key": "workbench.startupEditor.welcomePageInEmptyWorkbench" }, + { + "comment": [ + "This is the description for a setting. Values surrounded by single quotes are not to be translated." + ], + "key": "workbench.startupEditor.terminal" + }, "workbench.startupEditor", "deprecationMessage", - "workbench.welcomePage.preferReducedMotion" + "workbench.welcomePage.preferReducedMotion", + "miWelcome", + "minWelcomeDescription", + "welcome", + "welcome.goBack", + "welcome.showAllWalkthroughs" ], "vs/workbench/contrib/welcomeWalkthrough/browser/walkThrough.contribution": [ "walkThrough.editor.label", @@ -4867,18 +5204,29 @@ ], "vs/workbench/contrib/welcomeViews/common/newFile.contribution": [ "Built-In", - "Create", - "welcome.newFile", "newFileTitle", "newFilePlaceholder", "file", "notebook", "change keybinding", "miNewFileWithName", - "miNewFile2" + "miNewFile2", + "Create", + "welcome.newFile" ], - "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline": [ - "document" + "vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution": [ + "editorHasCallHierarchyProvider", + "callHierarchyVisible", + "callHierarchyDirection", + "no.item", + "error", + "showIncomingCallsIcons", + "showOutgoingCallsIcon", + "close", + "title", + "title.incoming", + "title.outgoing", + "title.refocus" ], "vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution": [ "editorHasTypeHierarchyProvider", @@ -4886,36 +5234,17 @@ "typeHierarchyDirection", "no.item", "error", + "close", "title", "title.supertypes", "title.subtypes", - "title.refocusTypeHierarchy", - "close" - ], - "vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution": [ - "editorHasCallHierarchyProvider", - "callHierarchyVisible", - "callHierarchyDirection", - "no.item", - "error", - "title", - "title.incoming", - "showIncomingCallsIcons", - "title.outgoing", - "showOutgoingCallsIcon", - "title.refocus", - "close" + "title.refocusTypeHierarchy" ], - "vs/workbench/contrib/languageDetection/browser/languageDetection.contribution": [ - "status.autoDetectLanguage", - "langDetection.name", - "langDetection.aria", - "detectlang", - "noDetection" + "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline": [ + "document" ], "vs/workbench/contrib/outline/browser/outline.contribution": [ "outlineViewIcon", - "name", "outlineConfigurationTitle", "outline.showIcons", "outline.initialState", @@ -4949,7 +5278,43 @@ "filteredTypes.struct", "filteredTypes.event", "filteredTypes.operator", - "filteredTypes.typeParameter" + "filteredTypes.typeParameter", + "name" + ], + "vs/workbench/contrib/languageDetection/browser/languageDetection.contribution": [ + "status.autoDetectLanguage", + "langDetection.name", + "langDetection.aria", + "noDetection", + "detectlang" + ], + "vs/workbench/contrib/languageStatus/browser/languageStatus.contribution": [ + "langStatus.name", + "langStatus.aria", + "pin", + "unpin", + "aria.1", + "aria.2", + "name.pattern", + "reset" + ], + "vs/workbench/contrib/authentication/browser/authentication.contribution": [ + "authentication.id", + "authentication.label", + { + "key": "authenticationExtensionPoint", + "comment": [ + "'Contributes' means adds here" + ] + }, + "authenticationlabel", + "authenticationid", + "authentication", + "authentication.Placeholder", + "authentication.missingId", + "authentication.missingLabel", + "authentication.idConflict", + "loading" ], "vs/workbench/contrib/userDataSync/browser/userDataSync.contribution": [ { @@ -4977,20 +5342,23 @@ "settings sync", "show sync logs" ], - "vs/workbench/contrib/editSessions/browser/editSessions.contribution": [ - "continue working on", - "continue edit session in local folder", - "show log", - "continueOn.installAdditional", - "resuming working changes window", - "autoStoreWorkingChanges", - "check for pending cloud changes", + "vs/workbench/contrib/timeline/browser/timeline.contribution": [ + "timelineViewIcon", + "timelineOpenIcon", + "timelineConfigurationTitle", + "timeline.pageSize", + "timeline.pageOnScroll", + "files.openTimeline", + "timelineFilter", + "filterTimeline" + ], + "vs/workbench/contrib/editSessions/browser/editSessions.contribution": [ + "continueOn.installAdditional", + "resuming working changes window", + "autoStoreWorkingChanges", + "check for pending cloud changes", "store working changes", - "show cloud changes", "store your working changes", - "resume latest cloud changes", - "resume cloud changes", - "store working changes in cloud", "storing working changes", "checkingForWorkingChanges", "no cloud changes", @@ -5030,42 +5398,33 @@ "continueOnCloudChanges.promptForAuth", "continueOnCloudChanges.off", "continueOnCloudChanges", - "cloudChangesPartialMatchesEnabled" - ], - "vs/workbench/contrib/timeline/browser/timeline.contribution": [ - "timelineViewIcon", - "timelineOpenIcon", - "timelineConfigurationTitle", - "timeline.pageSize", - "timeline.pageOnScroll", - "files.openTimeline", - "timelineFilter", - "filterTimeline" - ], - "vs/workbench/contrib/languageStatus/browser/languageStatus.contribution": [ - "langStatus.name", - "langStatus.aria", - "pin", - "unpin", - "aria.1", - "aria.2", - "name.pattern", - "reset" + "cloudChangesPartialMatchesEnabled", + "continue working on", + "continue edit session in local folder", + "show log", + "show cloud changes", + "resume latest cloud changes", + "resume cloud changes", + "store working changes in cloud" ], "vs/workbench/contrib/workspaces/browser/workspaces.contribution": [ - "workspaceFound", + { + "key": "foundWorkspace", + "comment": [ + "{Locked=\"]({1})\"}" + ] + }, "openWorkspace", - "workspacesFound", + { + "key": "foundWorkspaces", + "comment": [ + "{Locked=\"]({0})\"}" + ] + }, "selectWorkspace", "selectToOpen", - "openWorkspace", - "alreadyOpen" - ], - "vs/workbench/contrib/deprecatedExtensionMigrator/browser/deprecatedExtensionMigrator.contribution": [ - "bracketPairColorizer.notification", - "bracketPairColorizer.notification.action.uninstall", - "bracketPairColorizer.notification.action.enableNative", - "bracketPairColorizer.notification.action.showMoreInfo" + "alreadyOpen", + "openWorkspace" ], "vs/workbench/contrib/workspace/browser/workspace.contribution": [ "openLooseFileWorkspaceDetails", @@ -5142,7 +5501,6 @@ "restrictedModeBannerMessageWindow", "restrictedModeBannerMessageFolder", "restrictedModeBannerMessageWorkspace", - "status.ariaTrustedWindow", "status.ariaUntrustedWindow", { "key": "status.tooltipUntrustedWindow2", @@ -5150,7 +5508,6 @@ "[abc]({n}) are links. Only translate `features are disabled` and `window is not trusted`. Do not change brackets and parentheses or {n}" ] }, - "status.ariaTrustedFolder", "status.ariaUntrustedFolder", { "key": "status.tooltipUntrustedFolder2", @@ -5158,7 +5515,6 @@ "[abc]({n}) are links. Only translate `features are disabled` and `folder is not trusted`. Do not change brackets and parentheses or {n}" ] }, - "status.ariaTrustedWorkspace", "status.ariaUntrustedWorkspace", { "key": "status.tooltipUntrustedWorkspace2", @@ -5167,10 +5523,8 @@ ] }, "status.WorkspaceTrust", + "untrusted", "workspaceTrustEditor", - "workspacesCategory", - "configureWorkspaceTrustSettings", - "manageWorkspaceTrust", "workspace.trust.description", "workspace.trust.startupPrompt.description", "workspace.trust.startupPrompt.always", @@ -5184,139 +5538,35 @@ "workspace.trust.untrustedFiles.prompt", "workspace.trust.untrustedFiles.open", "workspace.trust.untrustedFiles.newWindow", - "workspace.trust.emptyWindow.description" + "workspace.trust.emptyWindow.description", + "workspacesCategory", + "configureWorkspaceTrustSettings", + "manageWorkspaceTrust" ], - "vs/workbench/contrib/audioCues/browser/audioCues.contribution": [ - "audioCues.enabled.auto", - "audioCues.enabled.on", - "audioCues.enabled.off", - "audioCues.volume", - "audioCues.debouncePositionChanges", - "audioCues.lineHasBreakpoint", - "audioCues.lineHasInlineSuggestion", - "audioCues.lineHasError", - "audioCues.lineHasFoldedArea", - "audioCues.lineHasWarning", - "audioCues.onDebugBreak", - "audioCues.noInlayHints", - "audioCues.taskCompleted", - "audioCues.taskFailed", - "audioCues.terminalCommandFailed", - "audioCues.terminalQuickFix", - "audioCues.diffLineInserted", - "audioCues.diffLineDeleted", - "audioCues.diffLineModified", - "audioCues.notebookCellCompleted", - "audioCues.notebookCellFailed", - "audioCues.chatRequestSent", - "audioCues.chatResponsePending", - "audioCues.chatResponseReceived" + "vs/workbench/contrib/deprecatedExtensionMigrator/browser/deprecatedExtensionMigrator.contribution": [ + "bracketPairColorizer.notification", + "bracketPairColorizer.notification.action.uninstall", + "bracketPairColorizer.notification.action.enableNative", + "bracketPairColorizer.notification.action.showMoreInfo" ], "vs/workbench/contrib/share/browser/share.contribution": [ - "share", "generating link", "shareTextSuccess", "shareSuccess", "close", "open link", - "experimental.share.enabled" - ], - "vs/workbench/browser/workbench": [ - "loaderErrorNative" - ], - "vs/workbench/services/configuration/browser/configurationService": [ - "configurationDefaults.description", - "experimental", - "setting description" - ], - "vs/platform/workspace/common/workspace": [ - "codeWorkspace" - ], - "vs/workbench/electron-sandbox/window": [ - "restart", - "configure", - "learnMore", - "keychainWriteError", - "troubleshooting", - "runningTranslated", - "downloadArmBuild", - "proxyAuthRequired", - { - "key": "loginButton", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "username", - "password", - "proxyDetail", - "rememberCredentials", - "quitMessageMac", - "quitMessage", - "closeWindowMessage", - { - "key": "quitButtonLabel", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "exitButtonLabel", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "closeWindowButtonLabel", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "doNotAskAgain", - "shutdownErrorDetail", - "willShutdownDetail", - "shutdownErrorClose", - "shutdownErrorQuit", - "shutdownErrorReload", - "shutdownErrorLoad", - "shutdownTitleClose", - "shutdownTitleQuit", - "shutdownTitleReload", - "shutdownTitleLoad", - "shutdownForceClose", - "shutdownForceQuit", - "shutdownForceReload", - "shutdownForceLoad", - "loaderCycle", - "runningAsRoot", - "appRootWarning.banner", - "windows32eolmessage", - "windowseolBannerLearnMore", - "windowseolarialabel", - "learnMore", - "macoseolmessage", - "macoseolBannerLearnMore", - "macoseolarialabel", - "learnMore", - "resolveShellEnvironment", - "learnMore" - ], - "vs/workbench/services/remote/electron-sandbox/remoteAgentService": [ - "devTools", - "directUrl", - "connectionError" + "experimental.share.enabled", + "share" ], - "vs/platform/workspace/common/workspaceTrust": [ - "trusted", - "untrusted" - ], - "vs/workbench/services/userDataProfile/common/userDataProfile": [ - "defaultProfileIcon", - "profiles", - "profile" + "vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution": [ + "workbench.accounts.showEntitlements", + "workbench.chat.showWelcomeView" ], - "vs/workbench/services/log/electron-sandbox/logService": [ - "rendererLog" + "vs/workbench/electron-sandbox/actions/developerActions": [ + "toggleDevTools", + "configureRuntimeArguments", + "reloadWindowWithExtensionsDisabled", + "openUserDataFolder" ], "vs/platform/configuration/common/configurationRegistry": [ "defaultLanguageConfigurationOverrides.title", @@ -5330,35 +5580,25 @@ "config.property.duplicate", "config.policy.duplicate" ], - "vs/workbench/electron-sandbox/actions/developerActions": [ - "toggleDevTools", - "configureRuntimeArguments", - "reloadWindowWithExtensionsDisabled", - "openUserDataFolder" - ], "vs/workbench/electron-sandbox/actions/windowActions": [ - "closeWindow", { "key": "miCloseWindow", "comment": [ "&& denotes a mnemonic" ] }, - "zoomIn", { "key": "miZoomIn", "comment": [ "&& denotes a mnemonic" ] }, - "zoomOut", { "key": "miZoomOut", "comment": [ "&& denotes a mnemonic" ] }, - "zoomReset", { "key": "miZoomReset", "comment": [ @@ -5367,9 +5607,15 @@ }, "close", "close", - "switchWindowPlaceHolder", + "windowGroup", "windowDirtyAriaLabel", "current", + "current", + "switchWindowPlaceHolder", + "closeWindow", + "zoomIn", + "zoomOut", + "zoomReset", "switchWindow", "quickSwitchWindow" ], @@ -5384,13 +5630,12 @@ "productQualityType", "inputFocus" ], - "vs/workbench/common/configuration": [ - "applicationConfigurationTitle", - "workbenchConfigurationTitle", - "securityConfigurationTitle", - "security.allowedUNCHosts.patternErrorMessage", - "security.allowedUNCHosts", - "security.restrictUNCAccess" + "vs/workbench/electron-sandbox/actions/installActions": [ + "successIn", + "successFrom", + "shellCommand", + "install", + "uninstall" ], "vs/workbench/common/contextkeys": [ "workbenchState", @@ -5400,6 +5645,7 @@ "virtualWorkspace", "temporaryWorkspace", "isFullscreen", + "isAuxiliaryWindowFocusedContext", "embedderIdentifier", "activeEditorIsDirty", "activeEditorIsNotPreview", @@ -5407,6 +5653,7 @@ "activeEditorIsLastInGroup", "activeEditorIsPinned", "activeEditorIsReadonly", + "activeCompareEditorCanSwap", "activeEditorCanToggleReadonly", "activeEditorCanRevert", "activeEditor", @@ -5420,16 +5667,21 @@ "activeEditorGroupLast", "activeEditorGroupLocked", "multipleEditorGroups", + "editorPartMultipleEditorGroups", + "editorPartEditorGroupMaximized", + "isAuxiliaryEditorPart", "editorIsOpen", "inZenMode", - "isCenteredLayout", + "isMainEditorCenteredLayout", "splitEditorsVertically", - "editorAreaVisible", + "mainEditorAreaVisible", "editorTabsVisible", "sideBarVisible", "sideBarFocus", "activeViewlet", "statusBarFocused", + "titleBarStyle", + "titleBarVisible", "bannerFocused", "notificationFocus", "notificationCenterVisible", @@ -5454,44 +5706,114 @@ "resourceSet", "isFileSystemResource" ], - "vs/workbench/services/dialogs/browser/abstractFileDialogService": [ - "saveChangesDetail", - "saveChangesMessage", - "saveChangesMessages", - { - "key": "saveAll", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "save", - "comment": [ - "&& denotes a mnemonic" - ] - }, + "vs/workbench/common/configuration": [ + "applicationConfigurationTitle", + "workbenchConfigurationTitle", + "securityConfigurationTitle", + "problemsConfigurationTitle", + "security.allowedUNCHosts.patternErrorMessage", + "security.allowedUNCHosts", + "security.restrictUNCAccess" + ], + "vs/workbench/electron-sandbox/window": [ + "restart", + "configure", + "learnMore", + "keychainWriteError", + "troubleshooting", + "runningTranslated", + "downloadArmBuild", + "proxyAuthRequired", { - "key": "dontSave", + "key": "loginButton", "comment": [ "&& denotes a mnemonic" ] }, - "openFileOrFolder.title", - "openFile.title", - "openFolder.title", - "openWorkspace.title", - "filterName.workspace", - "saveFileAs.title", - "saveAsTitle", - "allFiles", - "noExt" + "username", + "password", + "proxyDetail", + "rememberCredentials", + "shutdownErrorDetail", + "willShutdownDetail", + "shutdownErrorClose", + "shutdownErrorQuit", + "shutdownErrorReload", + "shutdownErrorLoad", + "shutdownTitleClose", + "shutdownTitleQuit", + "shutdownTitleReload", + "shutdownTitleLoad", + "shutdownForceClose", + "shutdownForceQuit", + "shutdownForceReload", + "shutdownForceLoad", + "loaderCycle", + "runningAsRoot", + "appRootWarning.banner", + "macoseolmessage", + "learnMore", + "resolveShellEnvironment", + "learnMore", + "zoomOut", + "zoomIn", + "zoomReset", + "zoomResetLabel", + "zoomSettings", + "status.windowZoom", + "zoomNumber" ], - "vs/workbench/electron-sandbox/actions/installActions": [ - "shellCommand", - "install", - "successIn", - "uninstall", - "successFrom" + "vs/workbench/browser/workbench": [ + "loaderErrorNative" + ], + "vs/workbench/services/configuration/browser/configurationService": [ + "configurationDefaults.description", + "experimental", + "setting description" + ], + "vs/platform/workspace/common/workspace": [ + "codeWorkspace" + ], + "vs/workbench/services/remote/electron-sandbox/remoteAgentService": [ + "devTools", + "directUrl", + "connectionError" + ], + "vs/workbench/services/log/electron-sandbox/logService": [ + "rendererLog" + ], + "vs/workbench/services/files/electron-sandbox/diskFileSystemProvider": [ + "fileWatcher" + ], + "vs/workbench/services/userDataProfile/common/userDataProfile": [ + "defaultProfileIcon", + "profile", + "profiles" + ], + "vs/workbench/browser/parts/dialogs/dialogHandler": [ + "aboutDetail", + { + "key": "copy", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "ok" + ], + "vs/workbench/electron-sandbox/parts/dialogs/dialogHandler": [ + { + "key": "aboutDetail", + "comment": [ + "Electron, Chromium, Node.js and V8 are product names that need no translation" + ] + }, + { + "key": "copy", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "okButton" ], "vs/workbench/services/textfile/browser/textFileService": [ "textFileCreate.source", @@ -5502,38 +5824,56 @@ "deleted", "fileBinaryError", "confirmOverwrite", - "irreversible", + "overwriteIrreversible", { "key": "replaceButtonLabel", "comment": [ "&& denotes a mnemonic" ] + }, + "confirmMakeWriteable", + "confirmMakeWriteableDetail", + { + "key": "makeWriteableButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] } ], - "vs/workbench/browser/parts/dialogs/dialogHandler": [ - "aboutDetail", + "vs/workbench/services/dialogs/browser/abstractFileDialogService": [ + "saveChangesDetail", + "saveChangesMessage", + "saveChangesMessages", { - "key": "copy", + "key": "saveAll", "comment": [ "&& denotes a mnemonic" ] }, - "ok" - ], - "vs/workbench/electron-sandbox/parts/dialogs/dialogHandler": [ { - "key": "aboutDetail", + "key": "save", "comment": [ - "Electron, Chromium, Node.js and V8 are product names that need no translation" + "&& denotes a mnemonic" ] }, { - "key": "copy", + "key": "dontSave", "comment": [ "&& denotes a mnemonic" ] }, - "okButton" + "openFileOrFolder.title", + "openFile.title", + "openFolder.title", + "openWorkspace.title", + "filterName.workspace", + "saveFileAs.title", + "saveAsTitle", + "allFiles", + "noExt" + ], + "vs/workbench/services/extensionManagement/common/extensionManagement": [ + "extensionsConfigurationTitle" ], "vs/workbench/common/theme": [ "tabActiveBackground", @@ -5556,6 +5896,7 @@ "tabActiveUnfocusedBorderTop", "tabHoverBorder", "tabUnfocusedHoverBorder", + "tabDragAndDropBorder", "tabActiveModifiedBorder", "tabInactiveModifiedBorder", "unfocusedActiveModifiedBorder", @@ -5586,6 +5927,11 @@ "panelSectionHeaderForeground", "panelSectionHeaderBorder", "panelSectionBorder", + "panelStickyScrollBackground", + "panelStickyScrollBorder", + "panelStickyScrollShadow", + "outputViewBackground", + "outputViewStickyScrollBackground", "banner.background", "banner.foreground", "banner.iconForeground", @@ -5623,6 +5969,12 @@ "activityBarDragAndDropBorder", "activityBarBadgeBackground", "activityBarBadgeForeground", + "activityBarTop", + "activityBarTopActiveFocusBorder", + "activityBarTopActiveBackground", + "activityBarTopInActiveForeground", + "activityBarTopDragAndDropBorder", + "activityBarTopBackground", "profileBadgeBackground", "profileBadgeForeground", "statusBarItemHostBackground", @@ -5638,11 +5990,16 @@ "sideBarBackground", "sideBarForeground", "sideBarBorder", + "sideBarTitleBackground", "sideBarTitleForeground", "sideBarDragAndDropBackground", "sideBarSectionHeaderBackground", "sideBarSectionHeaderForeground", "sideBarSectionHeaderBorder", + "sideBarActivityBarTopBorder", + "sideBarStickyScrollBackground", + "sideBarStickyScrollBorder", + "sideBarStickyScrollShadow", "titleBarActiveForeground", "titleBarInactiveForeground", "titleBarActiveBackground", @@ -5673,227 +6030,6 @@ "windowActiveBorder", "windowInactiveBorder" ], - "vs/platform/theme/common/colorRegistry": [ - "foreground", - "disabledForeground", - "errorForeground", - "descriptionForeground", - "iconForeground", - "focusBorder", - "contrastBorder", - "activeContrastBorder", - "selectionBackground", - "textSeparatorForeground", - "textLinkForeground", - "textLinkActiveForeground", - "textPreformatForeground", - "textBlockQuoteBackground", - "textBlockQuoteBorder", - "textCodeBlockBackground", - "widgetShadow", - "widgetBorder", - "inputBoxBackground", - "inputBoxForeground", - "inputBoxBorder", - "inputBoxActiveOptionBorder", - "inputOption.hoverBackground", - "inputOption.activeBackground", - "inputOption.activeForeground", - "inputPlaceholderForeground", - "inputValidationInfoBackground", - "inputValidationInfoForeground", - "inputValidationInfoBorder", - "inputValidationWarningBackground", - "inputValidationWarningForeground", - "inputValidationWarningBorder", - "inputValidationErrorBackground", - "inputValidationErrorForeground", - "inputValidationErrorBorder", - "dropdownBackground", - "dropdownListBackground", - "dropdownForeground", - "dropdownBorder", - "buttonForeground", - "buttonSeparator", - "buttonBackground", - "buttonHoverBackground", - "buttonBorder", - "buttonSecondaryForeground", - "buttonSecondaryBackground", - "buttonSecondaryHoverBackground", - "badgeBackground", - "badgeForeground", - "scrollbarShadow", - "scrollbarSliderBackground", - "scrollbarSliderHoverBackground", - "scrollbarSliderActiveBackground", - "progressBarBackground", - "editorError.background", - "editorError.foreground", - "errorBorder", - "editorWarning.background", - "editorWarning.foreground", - "warningBorder", - "editorInfo.background", - "editorInfo.foreground", - "infoBorder", - "editorHint.foreground", - "hintBorder", - "sashActiveBorder", - "editorBackground", - "editorForeground", - "editorStickyScrollBackground", - "editorStickyScrollHoverBackground", - "editorWidgetBackground", - "editorWidgetForeground", - "editorWidgetBorder", - "editorWidgetResizeBorder", - "pickerBackground", - "pickerForeground", - "pickerTitleBackground", - "pickerGroupForeground", - "pickerGroupBorder", - "keybindingLabelBackground", - "keybindingLabelForeground", - "keybindingLabelBorder", - "keybindingLabelBottomBorder", - "editorSelectionBackground", - "editorSelectionForeground", - "editorInactiveSelection", - "editorSelectionHighlight", - "editorSelectionHighlightBorder", - "editorFindMatch", - "findMatchHighlight", - "findRangeHighlight", - "editorFindMatchBorder", - "findMatchHighlightBorder", - "findRangeHighlightBorder", - "searchEditor.queryMatch", - "searchEditor.editorFindMatchBorder", - "search.resultsInfoForeground", - "hoverHighlight", - "hoverBackground", - "hoverForeground", - "hoverBorder", - "statusBarBackground", - "activeLinkForeground", - "editorInlayHintForeground", - "editorInlayHintBackground", - "editorInlayHintForegroundTypes", - "editorInlayHintBackgroundTypes", - "editorInlayHintForegroundParameter", - "editorInlayHintBackgroundParameter", - "editorLightBulbForeground", - "editorLightBulbAutoFixForeground", - "diffEditorInserted", - "diffEditorRemoved", - "diffEditorInsertedLines", - "diffEditorRemovedLines", - "diffEditorInsertedLineGutter", - "diffEditorRemovedLineGutter", - "diffEditorOverviewInserted", - "diffEditorOverviewRemoved", - "diffEditorInsertedOutline", - "diffEditorRemovedOutline", - "diffEditorBorder", - "diffDiagonalFill", - "diffEditor.unchangedRegionBackground", - "diffEditor.unchangedRegionForeground", - "diffEditor.unchangedCodeBackground", - "listFocusBackground", - "listFocusForeground", - "listFocusOutline", - "listFocusAndSelectionOutline", - "listActiveSelectionBackground", - "listActiveSelectionForeground", - "listActiveSelectionIconForeground", - "listInactiveSelectionBackground", - "listInactiveSelectionForeground", - "listInactiveSelectionIconForeground", - "listInactiveFocusBackground", - "listInactiveFocusOutline", - "listHoverBackground", - "listHoverForeground", - "listDropBackground", - "highlight", - "listFocusHighlightForeground", - "invalidItemForeground", - "listErrorForeground", - "listWarningForeground", - "listFilterWidgetBackground", - "listFilterWidgetOutline", - "listFilterWidgetNoMatchesOutline", - "listFilterWidgetShadow", - "listFilterMatchHighlight", - "listFilterMatchHighlightBorder", - "treeIndentGuidesStroke", - "treeInactiveIndentGuidesStroke", - "tableColumnsBorder", - "tableOddRowsBackgroundColor", - "listDeemphasizedForeground", - "checkbox.background", - "checkbox.select.background", - "checkbox.foreground", - "checkbox.border", - "checkbox.select.border", - "quickInput.list.focusBackground deprecation", - "quickInput.listFocusForeground", - "quickInput.listFocusIconForeground", - "quickInput.listFocusBackground", - "menuBorder", - "menuForeground", - "menuBackground", - "menuSelectionForeground", - "menuSelectionBackground", - "menuSelectionBorder", - "menuSeparatorBackground", - "toolbarHoverBackground", - "toolbarHoverOutline", - "toolbarActiveBackground", - "snippetTabstopHighlightBackground", - "snippetTabstopHighlightBorder", - "snippetFinalTabstopHighlightBackground", - "snippetFinalTabstopHighlightBorder", - "breadcrumbsFocusForeground", - "breadcrumbsBackground", - "breadcrumbsFocusForeground", - "breadcrumbsSelectedForeground", - "breadcrumbsSelectedBackground", - "mergeCurrentHeaderBackground", - "mergeCurrentContentBackground", - "mergeIncomingHeaderBackground", - "mergeIncomingContentBackground", - "mergeCommonHeaderBackground", - "mergeCommonContentBackground", - "mergeBorder", - "overviewRulerCurrentContentForeground", - "overviewRulerIncomingContentForeground", - "overviewRulerCommonContentForeground", - "overviewRulerFindMatchForeground", - "overviewRulerSelectionHighlightForeground", - "minimapFindMatchHighlight", - "minimapSelectionOccurrenceHighlight", - "minimapSelectionHighlight", - "minimapInfo", - "overviewRuleWarning", - "minimapError", - "minimapBackground", - "minimapForegroundOpacity", - "minimapSliderBackground", - "minimapSliderHoverBackground", - "minimapSliderActiveBackground", - "problemsErrorIconForeground", - "problemsWarningIconForeground", - "problemsInfoIconForeground", - "chartsForeground", - "chartsLines", - "chartsRed", - "chartsBlue", - "chartsYellow", - "chartsOrange", - "chartsGreen", - "chartsPurple" - ], "vs/base/common/actions": [ "submenu.empty" ], @@ -5903,11 +6039,6 @@ "errorInvalidTaskConfiguration", "openWorkspaceConfigurationFile" ], - "vs/platform/keyboardLayout/common/keyboardConfig": [ - "keyboardConfigurationTitle", - "dispatch", - "mapAltGrToCtrlAlt" - ], "vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService": [ "commandVariable.noStringType", "inputVariable.noInputSection", @@ -5917,6 +6048,11 @@ "inputVariable.unknownType", "inputVariable.undefinedVariable" ], + "vs/platform/keyboardLayout/common/keyboardConfig": [ + "keyboardConfigurationTitle", + "dispatch", + "mapAltGrToCtrlAlt" + ], "vs/workbench/services/extensionManagement/common/extensionManagementService": [ "singleDependentError", "twoDependentsError", @@ -5939,10 +6075,10 @@ "&& denotes a mnemonic" ] }, - "extensionInstallWorkspaceTrustMessage", "extensionInstallWorkspaceTrustButton", "extensionInstallWorkspaceTrustContinueButton", "extensionInstallWorkspaceTrustManageButton", + "extensionInstallWorkspaceTrustMessage", "VS Code for Web", "limited support", { @@ -5958,7 +6094,31 @@ ] }, "non web extensions detail", - "non web extensions" + "non web extensions", + "main.notFound" + ], + "vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService": [ + "notFoundReleaseExtension", + "notFoundCompatibleDependency" + ], + "vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker": [ + "backupTrackerBackupFailed", + "backupTrackerConfirmFailed", + "backupErrorDetails", + { + "key": "ok", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "shutdownForceClose", + "shutdownForceQuit", + "shutdownForceReload", + "backupBeforeShutdownMessage", + "backupBeforeShutdownDetail", + "saveBeforeShutdown", + "revertBeforeShutdown", + "discardBackupsBeforeShutdown" ], "vs/workbench/services/workingCopy/common/workingCopyHistoryService": [ "default.source", @@ -5966,9 +6126,73 @@ "renamed.source", "join.workingCopyHistory" ], - "vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService": [ - "notFoundReleaseExtension", - "notFoundCompatibleDependency" + "vs/platform/action/common/actionCommonCategories": [ + "view", + "help", + "test", + "file", + "preferences", + { + "key": "developer", + "comment": [ + "A developer on Code itself or someone diagnosing issues in Code" + ] + } + ], + "vs/workbench/services/extensions/common/abstractExtensionService": [ + "looping", + "looping", + "extensionTestError", + "extensionStopVetoError", + "extensionStopVetoMessage", + "extensionStopVetoDetailsOne", + "extensionStopVetoDetailsMany", + "extensionService.autoRestart", + "extensionService.crash", + "restart", + "activation" + ], + "vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner": [ + "extensionCache.invalid", + "reloadWindow" + ], + "vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost": [ + "extensionHost.startupFailDebug", + "extensionHost.startupFail", + "reloadWindow", + "join.extensionDevelopment" + ], + "vs/workbench/contrib/localization/electron-sandbox/minimalTranslations": [ + "showLanguagePackExtensions", + "searchMarketplace", + "installAndRestartMessage", + "installAndRestart" + ], + "vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService": [ + "lifecycleVeto", + "unableToOpenWindowError", + "unableToOpenWindow", + "unableToOpenWindowDetail", + { + "key": "retry", + "comment": [ + "&& denotes a mnemonic" + ] + } + ], + "vs/workbench/contrib/localization/common/localization.contribution": [ + "vscode.extension.contributes.localizations", + "vscode.extension.contributes.localizations.languageId", + "vscode.extension.contributes.localizations.languageName", + "vscode.extension.contributes.localizations.languageNameLocalized", + "vscode.extension.contributes.localizations.translations", + "vscode.extension.contributes.localizations.translations.id", + "vscode.extension.contributes.localizations.translations.id.pattern", + "vscode.extension.contributes.localizations.translations.path", + "language id", + "localizations language name", + "localizations localized language name", + "localizations" ], "vs/editor/common/editorContextKeys": [ "editorTextFocus", @@ -5977,9 +6201,17 @@ "editorReadonly", "inDiffEditor", "isEmbeddedDiffEditor", + "inMultiDiffEditor", + "multiDiffEditorAllCollapsed", + "diffEditorHasChanges", "comparingMovedCode", "accessibleDiffViewerVisible", "diffEditorRenderSideBySideInlineBreakpointReached", + "diffEditorInlineMode", + "diffEditorOriginalWritable", + "diffEditorModifiedWritable", + "diffEditorOriginalUri", + "diffEditorModifiedUri", "editorColumnSelection", "editorHasSelection", "editorHasMultipleSelections", @@ -6011,80 +6243,31 @@ "editorHasMultipleDocumentFormattingProvider", "editorHasMultipleDocumentSelectionFormattingProvider" ], - "vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker": [ - "backupTrackerBackupFailed", - "backupTrackerConfirmFailed", - "backupErrorDetails", - "backupBeforeShutdownMessage", - "backupBeforeShutdownDetail", - "saveBeforeShutdown", - "revertBeforeShutdown", - "discardBackupsBeforeShutdown" - ], "vs/workbench/common/editor": [ "promptOpenWith.defaultEditor.displayName", "builtinProviderDisplayName", "openLargeFile", "configureEditorLargeFileConfirmation" ], - "vs/platform/action/common/actionCommonCategories": [ - "view", - "help", - "test", - "file", - "preferences", - { - "key": "developer", - "comment": [ - "A developer on Code itself or someone diagnosing issues in Code" - ] - } + "vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard": [ + "actions.pasteSelectionClipboard" ], - "vs/workbench/services/extensions/common/abstractExtensionService": [ - "looping", - "looping", - "extensionTestError", - "extensionStopVetoError", - "extensionStopVetoMessage", - "extensionStopVetoDetailsOne", - "extensionStopVetoDetailsMany", - "extensionService.autoRestart", - "extensionService.crash", - "restart" + "vs/workbench/contrib/codeEditor/electron-sandbox/startDebugTextMate": [ + "startDebugTextMate" ], "vs/workbench/contrib/logs/electron-sandbox/logsActions": [ "openLogsFolder", "openExtensionLogsFolder" ], - "vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner": [ - "extensionCache.invalid", - "reloadWindow" - ], - "vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost": [ - "extensionHost.startupFailDebug", - "extensionHost.startupFail", - "reloadWindow", - "join.extensionDevelopment" - ], - "vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard": [ - "actions.pasteSelectionClipboard" - ], "vs/workbench/browser/editor": [ "preview", "pinned" ], - "vs/workbench/contrib/codeEditor/electron-sandbox/startDebugTextMate": [ - "startDebugTextMate" - ], - "vs/workbench/contrib/extensions/common/runtimeExtensionsInput": [ - "extensionsInputName" - ], "vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor": [ "extensionHostProfileStart", "stopExtensionHostProfileStart", "saveExtensionHostProfile", - "saveprofile.dialogTitle", - "saveprofile.saveButton" + "saveprofile.dialogTitle" ], "vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction": [ "debugExtensionHost", @@ -6102,6 +6285,10 @@ "openExtensionsFolder", "cleanUpExtensionsFolder" ], + "vs/workbench/contrib/extensions/common/runtimeExtensionsInput": [ + "runtimeExtensionEditorLabelIcon", + "extensionsInputName" + ], "vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService": [ "status.profiler", "profilingExtensionHost", @@ -6121,26 +6308,39 @@ "unresponsive-exthost", "show" ], - "vs/workbench/contrib/localization/electron-sandbox/minimalTranslations": [ - "showLanguagePackExtensions", - "searchMarketplace", - "installAndRestartMessage", - "installAndRestart" - ], - "vs/workbench/contrib/localization/common/localization.contribution": [ - "vscode.extension.contributes.localizations", - "vscode.extension.contributes.localizations.languageId", - "vscode.extension.contributes.localizations.languageName", - "vscode.extension.contributes.localizations.languageNameLocalized", - "vscode.extension.contributes.localizations.translations", - "vscode.extension.contributes.localizations.translations.id", - "vscode.extension.contributes.localizations.translations.id.pattern", - "vscode.extension.contributes.localizations.translations.path" + "vs/workbench/contrib/issue/common/issue.contribution": [ + { + "key": "miReportIssue", + "comment": [ + "&& denotes a mnemonic", + "Translate this to \"Report Issue in English\" in all languages please!" + ] + }, + { + "key": "reportIssueInEnglish", + "comment": [ + "Translate this to \"Report Issue in English\" in all languages please!" + ] + } ], - "vs/workbench/services/dialogs/browser/simpleFileDialog": [ - "openLocalFile", - "saveLocalFile", - "openLocalFolder", + "vs/workbench/contrib/terminal/common/terminal": [ + "vscode.extension.contributes.terminal", + "vscode.extension.contributes.terminal.profiles", + "vscode.extension.contributes.terminal.profiles.id", + "vscode.extension.contributes.terminal.profiles.title", + "vscode.extension.contributes.terminal.types.icon", + "vscode.extension.contributes.terminal.types.icon.light", + "vscode.extension.contributes.terminal.types.icon.dark" + ], + "vs/workbench/contrib/issue/browser/issueQuickAccess": [ + "reportExtensionMarketplace", + "extensions", + "contributedIssuePage" + ], + "vs/workbench/services/dialogs/browser/simpleFileDialog": [ + "openLocalFile", + "saveLocalFile", + "openLocalFolder", "openLocalFileFolder", "remoteFileDialog.notConnectedToRemote", "remoteFileDialog.local", @@ -6158,30 +6358,6 @@ "remoteFileDialog.validateFileOnly", "remoteFileDialog.validateFolderOnly" ], - "vs/workbench/contrib/issue/common/issue.contribution": [ - { - "key": "reportIssueInEnglish", - "comment": [ - "Translate this to \"Report Issue in English\" in all languages please!" - ] - }, - { - "key": "miReportIssue", - "comment": [ - "&& denotes a mnemonic", - "Translate this to \"Report Issue in English\" in all languages please!" - ] - } - ], - "vs/workbench/contrib/terminal/common/terminal": [ - "vscode.extension.contributes.terminal", - "vscode.extension.contributes.terminal.profiles", - "vscode.extension.contributes.terminal.profiles.id", - "vscode.extension.contributes.terminal.profiles.title", - "vscode.extension.contributes.terminal.types.icon", - "vscode.extension.contributes.terminal.types.icon.light", - "vscode.extension.contributes.terminal.types.icon.dark" - ], "vs/editor/common/languages": [ "Array", "Boolean", @@ -6211,6 +6387,84 @@ "Variable", "symbolAriaLabel" ], + "vs/workbench/services/themes/common/themeConfiguration": [ + { + "key": "colorTheme", + "comment": [ + "{0} will become a link to another setting." + ] + }, + "colorThemeError", + { + "key": "preferredDarkColorTheme", + "comment": [ + "{0} will become a link to another setting." + ] + }, + "colorThemeError", + { + "key": "preferredLightColorTheme", + "comment": [ + "{0} will become a link to another setting." + ] + }, + "colorThemeError", + { + "key": "preferredHCDarkColorTheme", + "comment": [ + "{0} will become a link to another setting." + ] + }, + "colorThemeError", + { + "key": "preferredHCLightColorTheme", + "comment": [ + "{0} will become a link to another setting." + ] + }, + "colorThemeError", + { + "key": "detectColorScheme", + "comment": [ + "{0} and {1} will become links to other settings." + ] + }, + "workbenchColors", + "iconTheme", + "noIconThemeLabel", + "noIconThemeDesc", + "iconThemeError", + "productIconTheme", + "defaultProductIconThemeLabel", + "defaultProductIconThemeDesc", + "productIconThemeError", + { + "key": "autoDetectHighContrast", + "comment": [ + "{0} and {1} will become links to other settings." + ] + }, + "editorColors.comments", + "editorColors.strings", + "editorColors.keywords", + "editorColors.numbers", + "editorColors.types", + "editorColors.functions", + "editorColors.variables", + "editorColors.textMateRules", + "editorColors.semanticHighlighting", + "editorColors.semanticHighlighting.deprecationMessage", + { + "key": "editorColors.semanticHighlighting.deprecationMessageMarkdown", + "comment": [ + "{0} will become a link to another setting." + ] + }, + "editorColors", + "editorColors.semanticHighlighting.enabled", + "editorColors.semanticHighlighting.rules", + "semanticTokenColors" + ], "vs/workbench/services/userDataSync/common/userDataSync": [ "settings", "keybindings", @@ -6220,15 +6474,10 @@ "ui state label", "profiles", "workspace state label", - "sync category", "syncViewIcon", + "sync category", "download sync activity title" ], - "vs/workbench/contrib/tasks/common/tasks": [ - "tasks.taskRunningContext", - "tasksCategory", - "TaskDefinition.missingRequiredProperty" - ], "vs/workbench/contrib/performance/electron-sandbox/startupProfiler": [ "prof.message", "prof.detail", @@ -6248,6 +6497,24 @@ ] } ], + "vs/workbench/contrib/terminal/common/terminalContextKey": [ + "terminalFocusContextKey", + "terminalFocusInAnyContextKey", + "terminalEditorFocusContextKey", + "terminalCountContextKey", + "terminalTabsFocusContextKey", + "terminalShellTypeContextKey", + "terminalAltBufferActive", + "terminalSuggestWidgetVisible", + "terminalViewShowing", + "terminalTextSelectedContextKey", + "terminalTextSelectedInFocusedContextKey", + "terminalProcessSupportedContextKey", + "terminalTabsSingularSelectedContextKey", + "isSplitTerminalContextKey", + "inTerminalRunCommandPickerContextKey", + "terminalShellIntegrationEnabled" + ], "vs/workbench/contrib/tasks/common/taskService": [ "tasks.customExecutionSupported", "tasks.shellExecutionSupported", @@ -6256,14 +6523,64 @@ "tasks.serverlessWebContext" ], "vs/workbench/common/views": [ + "views log", "defaultViewIcon", "duplicateId", "treeView.notRegistered" ], + "vs/workbench/contrib/tasks/browser/terminalTaskSystem": [ + "TerminalTaskSystem.unknownError", + "TerminalTaskSystem.taskLoadReporting", + "dependencyCycle", + "dependencyFailed", + "TerminalTaskSystem.nonWatchingMatcher", + { + "key": "task.executingInFolder", + "comment": [ + "The workspace folder the task is running in", + "The task command line or label" + ] + }, + { + "key": "task.executing.shellIntegration", + "comment": [ + "The task command line or label" + ] + }, + { + "key": "task.executingInFolder", + "comment": [ + "The workspace folder the task is running in", + "The task command line or label" + ] + }, + { + "key": "task.executing.shell-integration", + "comment": [ + "The task command line or label" + ] + }, + { + "key": "task.executing", + "comment": [ + "The task command line or label" + ] + }, + "TerminalTaskSystem", + "unknownProblemMatcher", + "closeTerminal", + "reuseTerminal" + ], "vs/workbench/contrib/tasks/browser/abstractTaskService": [ - "ConfigureTaskRunnerAction.label", "tasks", "TaskService.pickBuildTaskForLabel", + "taskEvent", + "TaskService.skippingReconnection", + "TaskService.notConnecting", + "TaskService.reconnecting", + "TaskService.reconnected", + "TaskService.noTasks", + "TaskService.reconnectingTasks", "runTask.arg", "runTask.label", "runTask.type", @@ -6272,6 +6589,15 @@ "showOutput", "TaskServer.folderIgnored", "TaskService.providerUnavailable", + "taskService.gettingCachedTasks", + "taskService.getSavedTasks", + "taskService.getSavedTasks.reading", + "taskService.getSavedTasks.error", + "taskService.getSavedTasks.resolved", + "taskService.getSavedTasks.unresolved", + "taskService.removePersistentTask", + "taskService.setPersistentTask", + "savePersistentTask", "TaskService.noTestTask1", "TaskService.noTestTask2", "TaskService.noBuildTask1", @@ -6294,7 +6620,12 @@ "&& denotes a mnemonic" ] }, - "saveBeforeRun.dontSave", + { + "key": "saveBeforeRun.dontSave", + "comment": [ + "&& denotes a mnemonic" + ] + }, "TaskSystem.activeSame.noBackground", "terminateTask", "restartTask", @@ -6314,7 +6645,6 @@ "TaskSystem.versionWorkspaceFile", "TasksSystem.locationUserConfig", "TaskSystem.versionSettings", - "TaskSystem.workspaceFolderError", "TaskSystem.configurationErrors", "taskService.ignoreingFolder", "TaskSystem.invalidTaskJson", @@ -6358,131 +6688,134 @@ "taskService.upgradeVersion", "taskService.upgradeVersionPlural", "taskService.openDiff", - "taskService.openDiffs" + "taskService.openDiffs", + "ConfigureTaskRunnerAction.label" + ], + "vs/platform/accessibilitySignal/browser/accessibilitySignalService": [ + "accessibilitySignals.positionHasError.name", + "accessibility.signals.positionHasError", + "accessibilitySignals.positionHasWarning.name", + "accessibility.signals.positionHasWarning", + "accessibilitySignals.lineHasError.name", + "accessibility.signals.lineHasError", + "accessibilitySignals.lineHasWarning.name", + "accessibility.signals.lineHasWarning", + "accessibilitySignals.lineHasFoldedArea.name", + "accessibility.signals.lineHasFoldedArea", + "accessibilitySignals.lineHasBreakpoint.name", + "accessibility.signals.lineHasBreakpoint", + "accessibilitySignals.lineHasInlineSuggestion.name", + "accessibilitySignals.terminalQuickFix.name", + "accessibility.signals.terminalQuickFix", + "accessibilitySignals.onDebugBreak.name", + "accessibility.signals.onDebugBreak", + "accessibilitySignals.noInlayHints", + "accessibility.signals.noInlayHints", + "accessibilitySignals.taskCompleted", + "accessibility.signals.taskCompleted", + "accessibilitySignals.taskFailed", + "accessibility.signals.taskFailed", + "accessibilitySignals.terminalCommandFailed", + "accessibility.signals.terminalCommandFailed", + "accessibilitySignals.terminalBell", + "accessibility.signals.terminalBell", + "accessibilitySignals.notebookCellCompleted", + "accessibility.signals.notebookCellCompleted", + "accessibilitySignals.notebookCellFailed", + "accessibility.signals.notebookCellFailed", + "accessibilitySignals.diffLineInserted", + "accessibilitySignals.diffLineDeleted", + "accessibilitySignals.diffLineModified", + "accessibilitySignals.chatRequestSent", + "accessibility.signals.chatRequestSent", + "accessibilitySignals.chatResponseReceived", + "accessibilitySignals.progress", + "accessibility.signals.progress", + "accessibilitySignals.clear", + "accessibility.signals.clear", + "accessibilitySignals.save", + "accessibility.signals.save", + "accessibilitySignals.format", + "accessibility.signals.format", + "accessibilitySignals.voiceRecordingStarted", + "accessibilitySignals.voiceRecordingStopped" ], - "vs/workbench/contrib/tasks/browser/terminalTaskSystem": [ - "TerminalTaskSystem.unknownError", - "TerminalTaskSystem.taskLoadReporting", - "dependencyCycle", - "dependencyFailed", - "TerminalTaskSystem.nonWatchingMatcher", - { - "key": "task.executingInFolder", - "comment": [ - "The workspace folder the task is running in", - "The task command line or label" - ] - }, - { - "key": "task.executing.shellIntegration", - "comment": [ - "The task command line or label" - ] - }, - { - "key": "task.executingInFolder", - "comment": [ - "The workspace folder the task is running in", - "The task command line or label" - ] - }, - { - "key": "task.executing.shell-integration", - "comment": [ - "The task command line or label" - ] - }, + "vs/workbench/contrib/tasks/common/tasks": [ + "tasks.taskRunningContext", + "TaskDefinition.missingRequiredProperty", + "tasksCategory" + ], + "vs/workbench/contrib/webview/electron-sandbox/webviewCommands": [ + "openToolsDescription", + "iframeWebviewAlert", + "openToolsLabel" + ], + "vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands": [ + "mergeEditor.enterJSON", + "mergeEditor", + "merge.dev.openState", + "merge.dev.openSelectionInTemporaryMergeEditor" + ], + "vs/workbench/contrib/localHistory/electron-sandbox/localHistoryCommands": [ + "revealInWindows", + "revealInMac", + "openContainer" + ], + "vs/workbench/contrib/multiDiffEditor/browser/actions": [ + "goToFile", + "collapseAllDiffs", + "ExpandAllDiffs" + ], + "vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver": [ + "viewChanges" + ], + "vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions": [ + "holdForSpeech" + ], + "vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput": [ + "name", { - "key": "task.executing", + "key": "files", "comment": [ - "The task command line or label" + "the number of files being shown" ] - }, - "TerminalTaskSystem", - "unknownProblemMatcher", - "closeTerminal", - "reuseTerminal" - ], - "vs/platform/audioCues/browser/audioCueService": [ - "audioCues.lineHasError.name", - "audioCues.lineHasWarning.name", - "audioCues.lineHasFoldedArea.name", - "audioCues.lineHasBreakpoint.name", - "audioCues.lineHasInlineSuggestion.name", - "audioCues.terminalQuickFix.name", - "audioCues.onDebugBreak.name", - "audioCues.noInlayHints", - "audioCues.taskCompleted", - "audioCues.taskFailed", - "audioCues.terminalCommandFailed", - "audioCues.terminalBell", - "audioCues.notebookCellCompleted", - "audioCues.notebookCellFailed", - "audioCues.diffLineInserted", - "audioCues.diffLineDeleted", - "audioCues.diffLineModified", - "audioCues.chatRequestSent", - "audioCues.chatResponseReceived", - "audioCues.chatResponsePending" - ], - "vs/workbench/contrib/webview/electron-sandbox/webviewCommands": [ - "openToolsLabel", - "iframeWebviewAlert" - ], - "vs/workbench/contrib/terminal/common/terminalContextKey": [ - "terminalFocusContextKey", - "terminalFocusInAnyContextKey", - "terminalEditorFocusContextKey", - "terminalCountContextKey", - "terminalTabsFocusContextKey", - "terminalShellTypeContextKey", - "terminalAltBufferActive", - "terminalSuggestWidgetVisible", - "terminalViewShowing", - "terminalTextSelectedContextKey", - "terminalTextSelectedInFocusedContextKey", - "terminalProcessSupportedContextKey", - "terminalTabsSingularSelectedContextKey", - "isSplitTerminalContextKey", - "inTerminalRunCommandPickerContextKey", - "terminalShellIntegrationEnabled" - ], - "vs/workbench/contrib/localHistory/electron-sandbox/localHistoryCommands": [ - "revealInWindows", - "revealInMac", - "openContainer" - ], - "vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands": [ - "mergeEditor", - "merge.dev.openState", - "mergeEditor.enterJSON", - "merge.dev.openSelectionInTemporaryMergeEditor" + } ], "vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions": [ "voiceChatGettingReady", "voiceChatInProgress", "quickVoiceChatInProgress", "inlineVoiceChatInProgress", + "terminalVoiceChatInProgress", "voiceChatInViewInProgress", "voiceChatInEditorInProgress", + "listening", + "confirmInstallDetail", + "voice.keywordActivation.off", + "voice.keywordActivation.chatInView", + "voice.keywordActivation.quickChat", + "voice.keywordActivation.inlineChat", + "voice.keywordActivation.chatInContext", + "voice.keywordActivation", + "keywordActivation.status.name", + "keywordActivation.status.active", + "keywordActivation.status.inactive", "workbench.action.chat.voiceChatInView.label", + "workbench.action.chat.holdToVoiceChatInChatView.label", "workbench.action.chat.inlineVoiceChat", "workbench.action.chat.quickVoiceChat.label", - "workbench.action.chat.startVoiceChat", - "workbench.action.chat.stopVoiceChat.label", - "workbench.action.chat.stopVoiceChatInChatView.label", - "workbench.action.chat.stopVoiceChatInChatEditor.label", - "workbench.action.chat.stopQuickVoiceChat.label", - "workbench.action.chat.stopInlineVoiceChat.label", - "workbench.action.chat.stopAndAcceptVoiceChat.label" + "workbench.action.chat.startVoiceChat.label", + "workbench.action.chat.startVoiceChat.label", + "workbench.action.chat.stopListening.label", + "workbench.action.chat.stopListeningAndSubmit.label" + ], + "vs/workbench/api/common/extHostTelemetry": [ + "extensionTelemetryLog" ], "vs/workbench/api/common/extHostExtensionService": [ "extensionTestError1", "extensionTestError" ], - "vs/workbench/api/common/extHostTelemetry": [ - "extensionTelemetryLog" - ], "vs/workbench/api/common/extHostWorkspace": [ "updateerror" ], @@ -6498,8 +6831,9 @@ "worker", "local" ], - "vs/workbench/api/node/extHostDebugService": [ - "debug.terminal.title" + "vs/workbench/api/common/extHostLanguageModels": [ + "chatAccessWithJustification", + "chatAccess" ], "vs/platform/terminal/node/terminalProcess": [ "launchFail.cwdNotDirectory", @@ -6507,10 +6841,8 @@ "launchFail.executableDoesNotExist", "launchFail.executableIsNotFileOrSymlink" ], - "vs/platform/shell/node/shellEnv": [ - "resolveShellEnvTimeout", - "resolveShellEnvError", - "resolveShellEnvExitError" + "vs/workbench/api/node/extHostDebugService": [ + "debug.terminal.title" ], "vs/platform/dialogs/electron-main/dialogMainService": [ "open", @@ -6524,6 +6856,11 @@ ] } ], + "vs/platform/shell/node/shellEnv": [ + "resolveShellEnvTimeout", + "resolveShellEnvError", + "resolveShellEnvExitError" + ], "vs/platform/externalTerminal/node/externalTerminalService": [ "console.title", "mac.terminal.script.failed", @@ -6586,6 +6923,38 @@ "cantUninstall", "sourceMissing" ], + "vs/platform/workspaces/electron-main/workspacesHistoryMainService": [ + { + "key": "clearButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "cancel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "confirmClearRecentsMessage", + "confirmClearDetail", + "newWindow", + "newWindowDesc", + "recentFoldersAndWorkspaces", + "recentFolders", + "untitledWorkspace", + "workspaceName" + ], + "vs/platform/workspaces/electron-main/workspacesManagementMainService": [ + { + "key": "ok", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "workspaceOpenedMessage", + "workspaceOpenedDetail" + ], "vs/platform/windows/electron-main/windowsMainService": [ { "key": "ok", @@ -6619,30 +6988,9 @@ "confirmOpenDetail", "doNotAskAgain" ], - "vs/platform/workspaces/electron-main/workspacesManagementMainService": [ - { - "key": "ok", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "workspaceOpenedMessage", - "workspaceOpenedDetail" - ], - "vs/platform/workspaces/electron-main/workspacesHistoryMainService": [ - "newWindow", - "newWindowDesc", - "recentFoldersAndWorkspaces", - "recentFolders", - "untitledWorkspace", - "workspaceName" - ], "vs/platform/files/common/io": [ "fileTooLargeError" ], - "vs/base/browser/ui/button/button": [ - "button dropdown more actions" - ], "vs/platform/extensions/common/extensionValidator": [ "extensionDescription.publisher", "extensionDescription.name", @@ -6696,6 +7044,28 @@ "twoIndirectDependentsError", "multipleIndirectDependentsError" ], + "vs/platform/extensionManagement/node/extensionManagementUtil": [ + "invalidManifest" + ], + "vs/base/browser/ui/button/button": [ + "button dropdown more actions" + ], + "vs/platform/theme/common/iconRegistry": [ + "iconDefinition.fontId", + "iconDefinition.fontCharacter", + "widgetClose", + "previousChangeIcon", + "nextChangeIcon" + ], + "vs/base/browser/ui/tree/abstractTree": [ + "filter", + "fuzzySearch", + "type to filter", + "type to search", + "type to search", + "close", + "not found" + ], "vs/base/common/date": [ "date.fromNow.in", "date.fromNow.now", @@ -6750,10 +7120,16 @@ "date.fromNow.years.singular.fullWord", "date.fromNow.years.singular", "date.fromNow.years.plural.fullWord", - "date.fromNow.years.plural" - ], - "vs/platform/extensionManagement/node/extensionManagementUtil": [ - "invalidManifest" + "date.fromNow.years.plural", + "duration.ms.full", + "duration.ms", + "duration.s.full", + "duration.s", + "duration.m.full", + "duration.m", + "duration.h.full", + "duration.h", + "duration.d" ], "vs/platform/userDataSync/common/keybindingsSync": [ "errorInvalidSettings", @@ -6780,22 +7156,6 @@ "session expired", "turned off machine" ], - "vs/base/browser/ui/tree/abstractTree": [ - "filter", - "fuzzySearch", - "type to filter", - "type to search", - "type to search", - "close", - "not found" - ], - "vs/platform/theme/common/iconRegistry": [ - "iconDefinition.fontId", - "iconDefinition.fontCharacter", - "widgetClose", - "previousChangeIcon", - "nextChangeIcon" - ], "vs/editor/common/core/editorColorRegistry": [ "lineHighlight", "lineHighlightBorderBox", @@ -6805,6 +7165,10 @@ "symbolHighlightBorder", "caret", "editorCursorBackground", + "editorMultiCursorPrimaryForeground", + "editorMultiCursorPrimaryBackground", + "editorMultiCursorSecondaryForeground", + "editorMultiCursorSecondaryBackground", "editorWhitespaces", "editorLineNumbers", "editorIndentGuides", @@ -6865,22 +7229,6 @@ "editorUnicodeHighlight.border", "editorUnicodeHighlight.background" ], - "vs/editor/browser/widget/diffEditor/diffEditor.contribution": [ - "toggleCollapseUnchangedRegions", - "toggleShowMovedCodeBlocks", - "toggleUseInlineViewWhenSpaceIsLimited", - "useInlineViewWhenSpaceIsLimited", - "showMoves", - "diffEditor", - "switchSide", - "exitCompareMove", - "collapseAllUnchangedRegions", - "showAllUnchangedRegions", - "accessibleDiffViewer", - "editor.action.accessibleDiffViewer.next", - "Open Accessible Diff Viewer", - "editor.action.accessibleDiffViewer.prev" - ], "vs/platform/contextkey/common/scanner": [ "contextkey.scanner.hint.didYouMean1", "contextkey.scanner.hint.didYouMean2", @@ -6888,6 +7236,13 @@ "contextkey.scanner.hint.didYouForgetToOpenOrCloseQuote", "contextkey.scanner.hint.didYouForgetToEscapeSlash" ], + "vs/editor/browser/widget/diffEditor/diffEditor.contribution": [ + "useInlineViewWhenSpaceIsLimited", + "showMoves", + "revertHunk", + "revertSelection", + "Open Accessible Diff Viewer" + ], "vs/editor/browser/coreCommands": [ "stickydesc", "stickydesc", @@ -6901,10 +7256,6 @@ "selectFromAnchorToCursor", "cancelSelectionAnchor" ], - "vs/editor/contrib/caretOperations/browser/caretOperations": [ - "caret.moveLeft", - "caret.moveRight" - ], "vs/editor/contrib/bracketMatching/browser/bracketMatching": [ "overviewRulerBracketMatchForeground", "smartSelect.jumpBracket", @@ -6915,11 +7266,12 @@ "comment": [ "&& denotes a mnemonic" ] - } + }, + "smartSelect.selectToBracketDescription" ], - "vs/editor/browser/widget/codeEditorWidget": [ - "cursors.maximum", - "goToSetting" + "vs/editor/contrib/caretOperations/browser/caretOperations": [ + "caret.moveLeft", + "caret.moveRight" ], "vs/editor/contrib/caretOperations/browser/transpose": [ "transposeLetters.label" @@ -6943,11 +7295,6 @@ "actions.clipboard.copyLabel", "actions.clipboard.copyLabel", "actions.clipboard.copyLabel", - "copy as", - "copy as", - "share", - "share", - "share", { "key": "miPaste", "comment": [ @@ -6957,18 +7304,44 @@ "actions.clipboard.pasteLabel", "actions.clipboard.pasteLabel", "actions.clipboard.pasteLabel", - "actions.clipboard.copyWithSyntaxHighlightingLabel" + "actions.clipboard.copyWithSyntaxHighlightingLabel", + "copy as", + "copy as", + "share", + "share", + "share" + ], + "vs/editor/browser/widget/codeEditor/codeEditorWidget": [ + "cursors.maximum", + "goToSetting" ], "vs/editor/contrib/codeAction/browser/codeActionContributions": [ "showCodeActionHeaders", - "includeNearbyQuickfixes" + "includeNearbyQuickFixes" ], "vs/editor/contrib/codelens/browser/codelensController": [ "showLensOnLine", "placeHolder" ], + "vs/editor/contrib/comment/browser/comment": [ + "comment.line", + { + "key": "miToggleLineComment", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "comment.line.add", + "comment.line.remove", + "comment.block", + { + "key": "miToggleBlockComment", + "comment": [ + "&& denotes a mnemonic" + ] + } + ], "vs/editor/contrib/colorPicker/browser/standaloneColorPickerActions": [ - "showOrFocusStandaloneColorPicker", { "key": "mishowOrFocusStandaloneColorPicker", "comment": [ @@ -6986,25 +7359,15 @@ "comment": [ "Action that inserts color with standalone color picker" ] - } - ], - "vs/editor/contrib/comment/browser/comment": [ - "comment.line", - { - "key": "miToggleLineComment", - "comment": [ - "&& denotes a mnemonic" - ] }, - "comment.line.add", - "comment.line.remove", - "comment.block", - { - "key": "miToggleBlockComment", - "comment": [ - "&& denotes a mnemonic" - ] - } + "showOrFocusStandaloneColorPicker", + "showOrFocusStandaloneColorPickerDescription", + "hideColorPickerDescription", + "insertColorWithStandaloneColorPickerDescription" + ], + "vs/editor/contrib/cursorUndo/browser/cursorUndo": [ + "cursor.undo", + "cursor.redo" ], "vs/editor/contrib/contextmenu/browser/contextmenu": [ "context.minimap.minimap", @@ -7018,16 +7381,10 @@ "context.minimap.slider.always", "action.showContextMenu.label" ], - "vs/editor/contrib/cursorUndo/browser/cursorUndo": [ - "cursor.undo", - "cursor.redo" - ], "vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution": [ + "pasteAs.kind", "pasteAs", - "pasteAs.id" - ], - "vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution": [ - "defaultProviderDescription" + "pasteAsText" ], "vs/editor/contrib/find/browser/findController": [ "too.large.for.replaceall", @@ -7038,10 +7395,6 @@ "&& denotes a mnemonic" ] }, - "actions.find.isRegexOverride", - "actions.find.wholeWordOverride", - "actions.find.matchCaseOverride", - "actions.find.preserveCaseOverride", "startFindWithArgsAction", "startFindWithSelectionAction", "findNextMatchAction", @@ -7061,6 +7414,9 @@ ] } ], + "vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution": [ + "defaultProviderDescription" + ], "vs/editor/contrib/fontZoom/browser/fontZoom": [ "EditorFontZoomIn.label", "EditorFontZoomOut.label", @@ -7090,24 +7446,23 @@ "formatDocument.label", "formatSelection.label" ], + "vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition": [ + "multipleResults" + ], "vs/editor/contrib/gotoSymbol/browser/goToCommands": [ "peek.submenu", "def.title", "noResultWord", "generic.noResults", - "actions.goToDecl.label", { "key": "miGotoDefinition", "comment": [ "&& denotes a mnemonic" ] }, - "actions.goToDeclToSide.label", - "actions.previewDecl.label", "decl.title", "decl.noResultWord", "decl.generic.noResults", - "actions.goToDeclaration.label", { "key": "miGotoDeclaration", "comment": [ @@ -7116,32 +7471,26 @@ }, "decl.noResultWord", "decl.generic.noResults", - "actions.peekDecl.label", "typedef.title", "goToTypeDefinition.noResultWord", "goToTypeDefinition.generic.noResults", - "actions.goToTypeDefinition.label", { "key": "miGotoTypeDefinition", "comment": [ "&& denotes a mnemonic" ] }, - "actions.peekTypeDefinition.label", "impl.title", "goToImplementation.noResultWord", "goToImplementation.generic.noResults", - "actions.goToImplementation.label", { "key": "miGotoImplementation", "comment": [ "&& denotes a mnemonic" ] }, - "actions.peekImplementation.label", "references.no", "references.noGeneric", - "goToReferences.label", { "key": "miGotoReference", "comment": [ @@ -7149,15 +7498,22 @@ ] }, "ref.title", - "references.action.label", "ref.title", - "label.generic", "generic.title", "generic.noResult", - "ref.title" - ], - "vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition": [ - "multipleResults" + "ref.title", + "actions.goToDecl.label", + "actions.goToDeclToSide.label", + "actions.previewDecl.label", + "actions.goToDeclaration.label", + "actions.peekDecl.label", + "actions.goToTypeDefinition.label", + "actions.peekTypeDefinition.label", + "actions.goToImplementation.label", + "actions.peekImplementation.label", + "goToReferences.label", + "references.action.label", + "label.generic" ], "vs/editor/contrib/gotoError/browser/gotoError": [ "markerAction.next.label", @@ -7196,96 +7552,22 @@ "changeTabDisplaySize", "detectIndentation", "editor.reindentlines", - "editor.reindentselectedlines" - ], - "vs/editor/contrib/hover/browser/hover": [ - { - "key": "showOrFocusHover", - "comment": [ - "Label for action that will trigger the showing/focusing of a hover in the editor.", - "If the hover is not visible, it will show the hover.", - "This allows for users to show the hover without using the mouse.", - "If the hover is already visible, it will take focus." - ] - }, - { - "key": "showDefinitionPreviewHover", - "comment": [ - "Label for action that will trigger the showing of definition preview hover in the editor.", - "This allows for users to show the definition preview hover without using the mouse." - ] - }, - { - "key": "scrollUpHover", - "comment": [ - "Action that allows to scroll up in the hover widget with the up arrow when the hover widget is focused." - ] - }, - { - "key": "scrollDownHover", - "comment": [ - "Action that allows to scroll down in the hover widget with the up arrow when the hover widget is focused." - ] - }, - { - "key": "scrollLeftHover", - "comment": [ - "Action that allows to scroll left in the hover widget with the left arrow when the hover widget is focused." - ] - }, - { - "key": "scrollRightHover", - "comment": [ - "Action that allows to scroll right in the hover widget with the right arrow when the hover widget is focused." - ] - }, - { - "key": "pageUpHover", - "comment": [ - "Action that allows to page up in the hover widget with the page up command when the hover widget is focused." - ] - }, - { - "key": "pageDownHover", - "comment": [ - "Action that allows to page down in the hover widget with the page down command when the hover widget is focused." - ] - }, - { - "key": "goToTopHover", - "comment": [ - "Action that allows to go to the top of the hover widget with the home command when the hover widget is focused." - ] - }, - { - "key": "goToBottomHover", - "comment": [ - "Action that allows to go to the bottom in the hover widget with the end command when the hover widget is focused." - ] - } - ], - "vs/editor/contrib/lineSelection/browser/lineSelection": [ - "expandLineSelection" - ], - "vs/editor/contrib/linkedEditing/browser/linkedEditing": [ - "linkedEditing.label", - "editorLinkedEditingBackground" + "editor.reindentselectedlines", + "indentationToSpacesDescription", + "indentationToTabsDescription", + "indentUsingTabsDescription", + "indentUsingSpacesDescription", + "changeTabDisplaySizeDescription", + "detectIndentationDescription", + "editor.reindentlinesDescription", + "editor.reindentselectedlinesDescription" ], "vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace": [ "InPlaceReplaceAction.previous.label", "InPlaceReplaceAction.next.label" ], - "vs/editor/contrib/links/browser/links": [ - "invalid.url", - "missing.url", - "links.navigate.executeCmd", - "links.navigate.follow", - "links.navigate.kb.meta.mac", - "links.navigate.kb.meta", - "links.navigate.kb.alt.mac", - "links.navigate.kb.alt", - "tooltip.explanation", - "label" + "vs/editor/contrib/lineSelection/browser/lineSelection": [ + "expandLineSelection" ], "vs/editor/contrib/linesOperations/browser/linesOperations": [ "lines.copyUp", @@ -7341,8 +7623,25 @@ "editor.transformToTitlecase", "editor.transformToSnakecase", "editor.transformToCamelcase", + "editor.transformToPascalcase", "editor.transformToKebabcase" ], + "vs/editor/contrib/linkedEditing/browser/linkedEditing": [ + "linkedEditing.label", + "editorLinkedEditingBackground" + ], + "vs/editor/contrib/links/browser/links": [ + "invalid.url", + "missing.url", + "links.navigate.executeCmd", + "links.navigate.follow", + "links.navigate.kb.meta.mac", + "links.navigate.kb.meta", + "links.navigate.kb.alt.mac", + "links.navigate.kb.alt", + "tooltip.explanation", + "label" + ], "vs/editor/contrib/multicursor/browser/multicursor": [ "cursorAdded", "cursorsAdded", @@ -7410,7 +7709,9 @@ "rename.failedApply", "rename.failed", "rename.label", - "enablePreview" + "enablePreview", + "focusNextRenameSuggestion", + "focusPreviousRenameSuggestion" ], "vs/editor/contrib/smartSelect/browser/smartSelect": [ "smartSelect.expand", @@ -7450,32 +7751,22 @@ "forceRetokenize" ], "vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode": [ + "toggle.tabMovesFocus.on", + "toggle.tabMovesFocus.off", { "key": "toggle.tabMovesFocus", "comment": [ "Turn on/off use of tab key for moving focus around VS Code" ] }, - "toggle.tabMovesFocus.on", - "toggle.tabMovesFocus.off" - ], - "vs/editor/contrib/unusualLineTerminators/browser/unusualLineTerminators": [ - "unusualLineTerminators.title", - "unusualLineTerminators.message", - "unusualLineTerminators.detail", - { - "key": "unusualLineTerminators.fix", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "unusualLineTerminators.ignore" + "tabMovesFocusDescriptions" ], "vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter": [ "warningIcon", "unicodeHighlighting.thisDocumentHasManyNonBasicAsciiUnicodeCharacters", "unicodeHighlighting.thisDocumentHasManyAmbiguousUnicodeCharacters", "unicodeHighlighting.thisDocumentHasManyInvisibleUnicodeCharacters", + "unicodeHighlight.configureUnicodeHighlightOptions", "unicodeHighlight.characterIsAmbiguousASCII", "unicodeHighlight.characterIsAmbiguous", "unicodeHighlight.characterIsInvisible", @@ -7494,17 +7785,28 @@ "action.unicodeHighlight.showExcludeOptions", "unicodeHighlight.excludeInvisibleCharFromBeingHighlighted", "unicodeHighlight.excludeCharFromBeingHighlighted", - "unicodeHighlight.allowCommonCharactersInLanguage", - "unicodeHighlight.configureUnicodeHighlightOptions" + "unicodeHighlight.allowCommonCharactersInLanguage" + ], + "vs/editor/contrib/unusualLineTerminators/browser/unusualLineTerminators": [ + "unusualLineTerminators.title", + "unusualLineTerminators.message", + "unusualLineTerminators.detail", + { + "key": "unusualLineTerminators.fix", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "unusualLineTerminators.ignore" + ], + "vs/editor/contrib/wordOperations/browser/wordOperations": [ + "deleteInsideWord" ], "vs/editor/contrib/wordHighlighter/browser/wordHighlighter": [ "wordHighlight.next.label", "wordHighlight.previous.label", "wordHighlight.trigger.label" ], - "vs/editor/contrib/wordOperations/browser/wordOperations": [ - "deleteInsideWord" - ], "vs/editor/contrib/readOnlyMessage/browser/contribution": [ "editor.simple.readonly", "editor.readonly" @@ -7529,6 +7831,12 @@ "tabFocusModeOffMsg", "tabFocusModeOffMsgNoKb", "showAccessibilityHelpAction", + "listSignalSoundsCommand", + "listAnnouncementsCommand", + "quickChatCommand", + "quickChatCommandNoKb", + "startInlineChatCommand", + "startInlineChatCommandNoKb", "inspectTokens", "gotoLineActionLabel", "helpQuickAccess", @@ -7550,7 +7858,10 @@ "invalid.url", "invalid.path.1", "invalid.url.fileschema", - "invalid.url.schema" + "invalid.url.schema", + "fileMatch", + "schema", + "jsonValidation" ], "vs/workbench/services/themes/common/colorExtensionPoint": [ "contributes.color", @@ -7568,7 +7879,13 @@ "invalid.description", "invalid.defaults", "invalid.defaults.highContrast", - "invalid.defaults.highContrastLight" + "invalid.defaults.highContrastLight", + "id", + "description", + "defaultDark", + "defaultLight", + "defaultHC", + "colors" ], "vs/workbench/services/themes/common/iconExtensionPoint": [ "contributes.icons", @@ -7611,7 +7928,7 @@ "invalid.semanticTokenScopes.scopes.value", "invalid.semanticTokenScopes.scopes.selector" ], - "vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint": [ + "vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint": [ "parseErrors", "formatError", "schema.openBracket", @@ -7690,6 +8007,9 @@ "vscode.extension.contributes.statusBarItems", "invalid" ], + "vs/workbench/api/browser/mainThreadLanguageModels": [ + "languageModelsAccountId" + ], "vs/workbench/api/browser/mainThreadCLICommands": [ "cannot be installed" ], @@ -7759,7 +8079,6 @@ "label" ], "vs/workbench/api/browser/mainThreadMessageService": [ - "extensionSource", "defaultSource", "manageExtension", "cancel", @@ -7809,25 +8128,6 @@ "remote.tunnelsView.elevationButton" ], "vs/workbench/api/browser/mainThreadAuthentication": [ - "noTrustedExtensions", - "manageTrustedExtensions.cancel", - { - "key": "accountLastUsedDate", - "comment": [ - "The placeholder {0} is a string with time information, such as \"3 days ago\"" - ] - }, - "notUsed", - "manageTrustedExtensions", - "manageExtensions", - "signOutMessage", - "signOutMessageSimple", - { - "key": "signOut", - "comment": [ - "&& denotes a mnemonic" - ] - }, "signedOut", "confirmRelogin", "confirmLogin", @@ -7836,14 +8136,19 @@ "comment": [ "&& denotes a mnemonic" ] - } + }, + "learnMore" + ], + "vs/workbench/browser/parts/titlebar/windowTitle": [ + "userIsAdmin", + "userIsSudo", + "devExtensionWindowTitlePrefix" ], "vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions": [ "toggleAuxiliaryIconRight", "toggleAuxiliaryIconRightOn", "toggleAuxiliaryIconLeft", "toggleAuxiliaryIconLeftOn", - "toggleAuxiliaryBar", "secondary sidebar", { "key": "secondary sidebar mnemonic", @@ -7851,9 +8156,10 @@ "&& denotes a mnemonic" ] }, - "focusAuxiliaryBar", "toggleSecondarySideBar", "toggleSecondarySideBar", + "toggleAuxiliaryBar", + "focusAuxiliaryBar", "hideAuxiliaryBar" ], "vs/workbench/browser/parts/panel/panelActions": [ @@ -7862,7 +8168,6 @@ "closeIcon", "togglePanelOffIcon", "togglePanelOnIcon", - "togglePanelVisibility", "toggle panel", { "key": "toggle panel mnemonic", @@ -7871,32 +8176,33 @@ ] }, "focusPanel", - "focusPanel", - "positionPanelLeft", "positionPanelLeftShort", - "positionPanelRight", "positionPanelRightShort", - "positionPanelBottom", "positionPanelBottomShort", - "alignPanelLeft", "alignPanelLeftShort", - "alignPanelRight", "alignPanelRightShort", - "alignPanelCenter", "alignPanelCenterShort", - "alignPanelJustify", "alignPanelJustifyShort", "positionPanel", "alignPanel", - "previousPanelView", - "nextPanelView", - "toggleMaximizedPanel", "maximizePanel", "minimizePanel", "panelMaxNotSupported", + "togglePanel", + "togglePanelVisibility", + "focusPanel", + "positionPanelLeft", + "positionPanelRight", + "positionPanelBottom", + "alignPanelLeft", + "alignPanelRight", + "alignPanelCenter", + "alignPanelJustify", + "previousPanelView", + "nextPanelView", + "toggleMaximizedPanel", "closePanel", "closeSecondarySideBar", - "togglePanel", "hidePanel", "movePanelToSecondarySideBar", "movePanelToSecondarySideBar", @@ -7949,6 +8255,7 @@ "vscode.extension.activationEvents.onTerminalProfile", "vscode.extension.activationEvents.onTerminalQuickFixRequest", "vscode.extension.activationEvents.onWalkthrough", + "vscode.extension.activationEvents.onIssueReporterOpened", "vscode.extension.activationEvents.star", "vscode.extension.badges", "vscode.extension.badges.url", @@ -7993,6 +8300,23 @@ "vscode.extension.pricing", "product.extensionEnabledApiProposals" ], + "vs/workbench/browser/parts/views/treeView": [ + "no-dataprovider", + "treeView.enableCollapseAll", + "treeView.enableRefresh", + "refresh", + "collapseAll", + "treeView.toggleCollapseAll", + "command-error" + ], + "vs/workbench/browser/parts/views/viewPaneContainer": [ + "views", + "viewMoveUp", + "viewMoveLeft", + "viewMoveDown", + "viewMoveRight", + "viewsMove" + ], "vs/workbench/contrib/debug/common/debug": [ "debugType", "debugConfigurationType", @@ -8016,10 +8340,13 @@ "watchItemType", "canViewMemory", "breakpointItemType", + "breakpointItemIsDataBytes", + "breakpointHasModes", "breakpointSupportsCondition", "loadedScriptsSupported", "loadedScriptsItemType", "focusedSessionIsAttach", + "focusedSessionIsNoDebug", "stepBackSupported", "restartFrameSupported", "stackFrameSupportsRestart", @@ -8030,6 +8357,7 @@ "debugExtensionsAvailable", "debugProtocolVariableMenuContext", "debugSetVariableSupported", + "debugSetDataBreakpointAddressSupported", "debugSetExpressionSupported", "breakWhenValueChangesSupported", "breakWhenValueIsAccessedSupported", @@ -8038,6 +8366,12 @@ "suspendDebuggeeSupported", "variableEvaluateNamePresent", "variableIsReadonly", + "variableValue", + "variableType", + "variableInterfaces", + "variableName", + "variableLanguage", + "variableExtensionId", "exceptionWidgetVisible", "multiSessionRepl", "multiSessionDebug", @@ -8064,23 +8398,17 @@ "explorerViewletCompressedLastFocus", "viewHasSomeCollapsibleItem" ], - "vs/workbench/browser/parts/views/viewPaneContainer": [ - "views", - "viewMoveUp", - "viewMoveLeft", - "viewMoveDown", - "viewMoveRight", - "viewsMove" - ], "vs/workbench/contrib/remote/browser/remoteExplorer": [ "remoteNoPorts", "noRemoteNoPorts", - "ports", "1forwardedPort", "nForwardedPorts", "remote.forwardedPorts.statusbarTextNone", "remote.forwardedPorts.statusbarTooltip", "status.forwardedPorts", + "remote.autoForwardPortsSource.fallback", + "remote.autoForwardPortsSource.fallback.switchBack", + "remote.autoForwardPortsSource.fallback.showPortSourceSetting", "remote.tunnelsView.automaticForward", { "key": "remote.tunnelsView.notificationLink2", @@ -8090,16 +8418,8 @@ }, "remote.tunnelsView.elevationMessage", "remote.tunnelsView.makePublic", - "remote.tunnelsView.elevationButton" - ], - "vs/workbench/browser/parts/views/treeView": [ - "no-dataprovider", - "treeView.enableCollapseAll", - "treeView.enableRefresh", - "refresh", - "collapseAll", - "treeView.toggleCollapseAll", - "command-error" + "remote.tunnelsView.elevationButton", + "ports" ], "vs/workbench/common/editor/sideBySideEditorInput": [ "sideBySideLabels" @@ -8159,7 +8479,6 @@ "currentProblem", "currentProblem", "showLanguageExtensions", - "changeMode", "noEditor", "languageDescription", "languageDescriptionConfigured", @@ -8170,11 +8489,9 @@ "pickLanguage", "currentAssociation", "pickLanguageToConfigure", - "changeEndOfLine", "noEditor", "noWritableCodeEditor", "pickEndOfLine", - "changeEncoding", "noEditor", "noEditor", "noFileEditor", @@ -8183,16 +8500,138 @@ "pickAction", "guessedEncoding", "pickEncodingForReopen", - "pickEncodingForSave" + "pickEncodingForSave", + "changeMode", + "changeEndOfLine", + "changeEncoding" ], - "vs/workbench/browser/parts/editor/editorActions": [ - "splitEditor", - "splitEditorOrthogonal", - "splitEditorGroupLeft", - "splitEditorGroupRight", - "splitEditorGroupUp", + "vs/workbench/browser/parts/editor/diffEditorCommands": [ + "compare", + "compare", + "compare.nextChange", + "compare.previousChange", + "toggleInlineView", + "swapDiffSides" + ], + "vs/workbench/browser/parts/editor/editorCommands": [ + "editorCommand.activeEditorMove.description", + "editorCommand.activeEditorMove.arg.name", + "editorCommand.activeEditorMove.arg.description", + "editorCommand.activeEditorCopy.description", + "editorCommand.activeEditorCopy.arg.name", + "editorCommand.activeEditorCopy.arg.description", + "splitEditorInGroup", + "joinEditorInGroup", + "toggleJoinEditorInGroup", + "toggleSplitEditorInGroupLayout", + "focusLeftSideEditor", + "focusRightSideEditor", + "focusOtherSideEditor", + "toggleEditorGroupLock", + "lockEditorGroup", + "unlockEditorGroup" + ], + "vs/editor/browser/editorExtensions": [ + { + "key": "miUndo", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "undo", + { + "key": "miRedo", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "redo", + { + "key": "miSelectAll", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "selectAll" + ], + "vs/workbench/browser/parts/editor/editorActions": [ "splitEditorGroupUp", "splitEditorGroupDown", + "closeEditor", + "unpinEditor", + "closeOneEditor", + "navigateForward", + { + "key": "miForward", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "navigateBack", + { + "key": "miBack", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "confirmClearRecentsMessage", + "confirmClearDetail", + { + "key": "clearButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "confirmClearEditorHistoryMessage", + "confirmClearDetail", + { + "key": "clearButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "splitEditorToLeftGroup", + { + "key": "miMoveEditorToNewWindow", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miCopyEditorToNewWindow", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miMoveEditorGroupToNewWindow", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miCopyEditorGroupToNewWindow", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miRestoreEditorsToMainWindow", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miNewEmptyEditorWindow", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "splitEditor", + "splitEditorOrthogonal", + "splitEditorGroupLeft", + "splitEditorGroupRight", + "splitEditorGroupUp", "splitEditorGroupDown", "joinTwoGroups", "joinAllGroups", @@ -8206,9 +8645,6 @@ "focusRightGroup", "focusAboveGroup", "focusBelowGroup", - "closeEditor", - "unpinEditor", - "closeOneEditor", "revertAndCloseActiveEditor", "closeEditorsToTheLeft", "closeAllEditors", @@ -8224,9 +8660,11 @@ "duplicateActiveGroupUp", "duplicateActiveGroupDown", "minimizeOtherEditorGroups", + "minimizeOtherEditorGroupsHideSidebar", "evenEditorGroups", "toggleEditorWidths", - "maximizeEditor", + "maximizeEditorHideSidebar", + "toggleMaximizeEditorGroup", "openNextEditor", "openPreviousEditor", "nextEditorInGroup", @@ -8234,21 +8672,7 @@ "firstEditorInGroup", "lastEditorInGroup", "navigateForward", - "navigateForward", - { - "key": "miForward", - "comment": [ - "&& denotes a mnemonic" - ] - }, "navigateBack", - "navigateBack", - { - "key": "miBack", - "comment": [ - "&& denotes a mnemonic" - ] - }, "navigatePrevious", "navigateForwardInEdits", "navigateBackInEdits", @@ -8260,14 +8684,6 @@ "navigateToLastNavigationLocation", "reopenClosedEditor", "clearRecentFiles", - "confirmClearRecentsMessage", - "confirmClearDetail", - { - "key": "clearButtonLabel", - "comment": [ - "&& denotes a mnemonic" - ] - }, "showEditorsInActiveGroup", "showAllEditors", "showAllEditorsByMostRecentlyUsed", @@ -8281,14 +8697,6 @@ "openNextRecentlyUsedEditorInGroup", "openPreviousRecentlyUsedEditorInGroup", "clearEditorHistory", - "confirmClearEditorHistoryMessage", - "confirmClearDetail", - { - "key": "clearButtonLabel", - "comment": [ - "&& denotes a mnemonic" - ] - }, "moveEditorLeft", "moveEditorRight", "moveEditorToPreviousGroup", @@ -8304,7 +8712,6 @@ "splitEditorToAboveGroup", "splitEditorToBelowGroup", "splitEditorToLeftGroup", - "splitEditorToLeftGroup", "splitEditorToRightGroup", "splitEditorToFirstGroup", "splitEditorToLastGroup", @@ -8321,117 +8728,55 @@ "newGroupAbove", "newGroupBelow", "toggleEditorType", - "reopenTextEditor" - ], - "vs/editor/browser/editorExtensions": [ - { - "key": "miUndo", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "undo", - { - "key": "miRedo", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "redo", - { - "key": "miSelectAll", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "selectAll" - ], - "vs/workbench/browser/parts/editor/editorCommands": [ - "editorCommand.activeEditorMove.description", - "editorCommand.activeEditorMove.arg.name", - "editorCommand.activeEditorMove.arg.description", - "editorCommand.activeEditorCopy.description", - "editorCommand.activeEditorCopy.arg.name", - "editorCommand.activeEditorCopy.arg.description", - "compare.nextChange", - "compare.previousChange", - "toggleInlineView", - "compare", - "splitEditorInGroup", - "joinEditorInGroup", - "toggleJoinEditorInGroup", - "toggleSplitEditorInGroupLayout", - "focusLeftSideEditor", - "focusRightSideEditor", - "focusOtherSideEditor", - "toggleEditorGroupLock", - "lockEditorGroup", - "unlockEditorGroup" - ], - "vs/workbench/browser/parts/editor/editorQuickAccess": [ - "noViewResults", - "entryAriaLabelWithGroupDirty", - "entryAriaLabelWithGroup", - "entryAriaLabelDirty", - "closeEditor" + "reopenTextEditor", + "moveEditorToNewWindow", + "copyEditorToNewWindow", + "moveEditorGroupToNewWindow", + "copyEditorGroupToNewWindow", + "restoreEditorsToMainWindow", + "newEmptyEditorWindow" ], "vs/workbench/browser/parts/editor/editorConfiguration": [ "interactiveWindow", "markdownPreview", + "simpleBrowser", + "livePreview", "workbench.editor.autoLockGroups", "workbench.editor.defaultBinaryEditor", "editor.editorAssociations", "editorLargeFileSizeConfirmation" ], + "vs/workbench/browser/parts/editor/editorQuickAccess": [ + "noViewResults", + "entryAriaLabelWithGroupDirty", + "entryAriaLabelWithGroup", + "entryAriaLabelDirty", + "closeEditor" + ], "vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart": [ + "activity bar position", "move second side bar left", "move second side bar right", "hide second side bar" ], - "vs/workbench/browser/parts/activitybar/activitybarPart": [ - "accountsViewBarIcon", - "menu", - "hideMenu", - "accounts", - "hideActivitBar", - "resetLocation", - "resetLocation", - "manage", - "accounts", - "manage", - "accounts" - ], "vs/workbench/browser/parts/panel/panelPart": [ - "resetLocation", - "resetLocation", - "panel.emptyMessage", - "moreActions", "panel position", "align panel", "hidePanel" ], - "vs/workbench/browser/parts/editor/editorGroupView": [ - "ariaLabelGroupActions", - "emptyEditorGroup", - "groupLabel", - "groupAriaLabel" - ], - "vs/workbench/browser/parts/editor/editorDropTarget": [ - "dropIntoEditorPrompt" - ], - "vs/workbench/browser/parts/statusbar/statusbarActions": [ - "hide", - "focusStatusBar" + "vs/workbench/browser/parts/sidebar/sidebarPart": [ + "toggleActivityBar" ], "vs/platform/actions/common/menuResetAction": [ "title" ], "vs/platform/actions/common/menuService": [ - "hide.label" + "hide.label", + "configure keybinding" ], - "vs/base/browser/ui/icons/iconSelectBox": [ - "iconSelect.placeholder", - "iconSelect.noResults" + "vs/workbench/browser/parts/statusbar/statusbarActions": [ + "hide", + "focusStatusBar" ], "vs/base/browser/ui/dialog/dialog": [ "ok", @@ -8442,21 +8787,23 @@ "dialogClose" ], "vs/workbench/services/preferences/browser/keybindingsEditorInput": [ + "keybindingsEditorLabelIcon", "keybindingsInputName" ], "vs/workbench/services/preferences/common/preferencesEditorInput": [ + "settingsEditorLabelIcon", "settingsEditor2InputName" ], "vs/workbench/services/preferences/common/preferencesModels": [ "commonlyUsed", "defaultKeybindingsHeader" ], - "vs/workbench/services/editor/common/editorResolverService": [ - "editor.editorAssociations" - ], "vs/workbench/services/textfile/common/textFileEditorModel": [ "textFileCreate.source" ], + "vs/workbench/services/editor/common/editorResolverService": [ + "editor.editorAssociations" + ], "vs/base/common/keybindingLabels": [ { "key": "ctrlKey", @@ -8579,13 +8926,50 @@ ] } ], - "vs/workbench/services/themes/common/fileIconThemeSchema": [ - "schema.folderExpanded", - "schema.folder", - "schema.file", - "schema.folderNames", - "schema.folderName", - "schema.folderNamesExpanded", + "vs/platform/keybinding/common/abstractKeybindingService": [ + "first.chord", + "next.chord", + "missing.chord", + "missing.chord" + ], + "vs/workbench/services/themes/common/colorThemeData": [ + "error.cannotparsejson", + "error.invalidformat", + { + "key": "error.invalidformat.colors", + "comment": [ + "{0} will be replaced by a path. Values in quotes should not be translated." + ] + }, + { + "key": "error.invalidformat.tokenColors", + "comment": [ + "{0} will be replaced by a path. Values in quotes should not be translated." + ] + }, + { + "key": "error.invalidformat.semanticTokenColors", + "comment": [ + "{0} will be replaced by a path. Values in quotes should not be translated." + ] + }, + "error.plist.invalidformat", + "error.cannotparse", + "error.cannotload" + ], + "vs/workbench/services/themes/common/fileIconThemeSchema": [ + "schema.folderExpanded", + "schema.folder", + "schema.file", + "schema.rootFolder", + "schema.rootFolderExpanded", + "schema.rootFolderNames", + "schema.folderName", + "schema.rootFolderNamesExpanded", + "schema.rootFolderNameExpanded", + "schema.folderNames", + "schema.folderName", + "schema.folderNamesExpanded", "schema.folderNameExpanded", "schema.fileExtensions", "schema.fileExtension", @@ -8618,37 +9002,6 @@ "error.cannotparseicontheme", "error.invalidformat" ], - "vs/workbench/services/themes/common/colorThemeData": [ - "error.cannotparsejson", - "error.invalidformat", - { - "key": "error.invalidformat.colors", - "comment": [ - "{0} will be replaced by a path. Values in quotes should not be translated." - ] - }, - { - "key": "error.invalidformat.tokenColors", - "comment": [ - "{0} will be replaced by a path. Values in quotes should not be translated." - ] - }, - { - "key": "error.invalidformat.semanticTokenColors", - "comment": [ - "{0} will be replaced by a path. Values in quotes should not be translated." - ] - }, - "error.plist.invalidformat", - "error.cannotparse", - "error.cannotload" - ], - "vs/platform/keybinding/common/abstractKeybindingService": [ - "first.chord", - "next.chord", - "missing.chord", - "missing.chord" - ], "vs/workbench/services/themes/common/colorThemeSchema": [ "schema.token.settings", "schema.token.foreground", @@ -8664,25 +9017,6 @@ "schema.supportsSemanticHighlighting", "schema.semanticTokenColors" ], - "vs/workbench/services/themes/common/themeExtensionPoints": [ - "vscode.extension.contributes.themes", - "vscode.extension.contributes.themes.id", - "vscode.extension.contributes.themes.label", - "vscode.extension.contributes.themes.uiTheme", - "vscode.extension.contributes.themes.path", - "vscode.extension.contributes.iconThemes", - "vscode.extension.contributes.iconThemes.id", - "vscode.extension.contributes.iconThemes.label", - "vscode.extension.contributes.iconThemes.path", - "vscode.extension.contributes.productIconThemes", - "vscode.extension.contributes.productIconThemes.id", - "vscode.extension.contributes.productIconThemes.label", - "vscode.extension.contributes.productIconThemes.path", - "reqarray", - "reqpath", - "reqid", - "invalid.path.1" - ], "vs/workbench/services/themes/browser/productIconThemeData": [ "error.parseicondefs", "defaultTheme", @@ -8707,85 +9041,34 @@ "schema.font-style", "schema.iconDefinitions" ], - "vs/workbench/services/themes/common/themeConfiguration": [ - "colorTheme", - "colorThemeError", - { - "key": "preferredDarkColorTheme", - "comment": [ - "{0} will become a link to another setting." - ] - }, - "colorThemeError", - { - "key": "preferredLightColorTheme", - "comment": [ - "{0} will become a link to another setting." - ] - }, - "colorThemeError", - { - "key": "preferredHCDarkColorTheme", - "comment": [ - "{0} will become a link to another setting." - ] - }, - "colorThemeError", - { - "key": "preferredHCLightColorTheme", - "comment": [ - "{0} will become a link to another setting." - ] - }, - "colorThemeError", - { - "key": "detectColorScheme", - "comment": [ - "{0} and {1} will become links to other settings." - ] - }, - "workbenchColors", - "iconTheme", - "noIconThemeLabel", - "noIconThemeDesc", - "iconThemeError", - "productIconTheme", - "defaultProductIconThemeLabel", - "defaultProductIconThemeDesc", - "productIconThemeError", - { - "key": "autoDetectHighContrast", - "comment": [ - "{0} and {1} will become links to other settings." - ] - }, - "editorColors.comments", - "editorColors.strings", - "editorColors.keywords", - "editorColors.numbers", - "editorColors.types", - "editorColors.functions", - "editorColors.variables", - "editorColors.textMateRules", - "editorColors.semanticHighlighting", - "editorColors.semanticHighlighting.deprecationMessage", - { - "key": "editorColors.semanticHighlighting.deprecationMessageMarkdown", - "comment": [ - "{0} will become a link to another setting." - ] - }, - "editorColors", - "editorColors.semanticHighlighting.enabled", - "editorColors.semanticHighlighting.rules", - "semanticTokenColors" + "vs/workbench/services/themes/common/themeExtensionPoints": [ + "vscode.extension.contributes.themes", + "vscode.extension.contributes.themes.id", + "vscode.extension.contributes.themes.label", + "vscode.extension.contributes.themes.uiTheme", + "vscode.extension.contributes.themes.path", + "vscode.extension.contributes.iconThemes", + "vscode.extension.contributes.iconThemes.id", + "vscode.extension.contributes.iconThemes.label", + "vscode.extension.contributes.iconThemes.path", + "vscode.extension.contributes.productIconThemes", + "vscode.extension.contributes.productIconThemes.id", + "vscode.extension.contributes.productIconThemes.label", + "vscode.extension.contributes.productIconThemes.path", + "color themes", + "file icon themes", + "product icon themes", + "themes", + "reqarray", + "reqpath", + "reqid", + "invalid.path.1" ], "vs/workbench/services/extensionManagement/browser/extensionBisect": [ "I cannot reproduce", "This is Bad", "bisect.singular", "bisect.plural", - "title.start", "msg.start", "detail.start", { @@ -8794,7 +9077,6 @@ "&& denotes a mnemonic" ] }, - "title.isBad", "done.msg", "done.detail2", "done.msg", @@ -8833,6 +9115,8 @@ "&& denotes a mnemonic" ] }, + "title.start", + "title.isBad", "title.stop" ], "vs/workbench/services/userDataProfile/browser/settingsResource": [ @@ -8845,8 +9129,8 @@ "snippets", "exclude" ], - "vs/workbench/services/userDataProfile/browser/globalStateResource": [ - "globalState" + "vs/workbench/services/userDataProfile/browser/tasksResource": [ + "tasks" ], "vs/workbench/services/userDataProfile/browser/extensionsResource": [ "extensions", @@ -8854,8 +9138,8 @@ "exclude", "exclude" ], - "vs/workbench/services/userDataProfile/browser/tasksResource": [ - "tasks" + "vs/workbench/services/userDataProfile/browser/globalStateResource": [ + "globalState" ], "vs/workbench/services/userDataProfile/common/userDataProfileIcons": [ "settingsViewBarIcon" @@ -8871,11 +9155,14 @@ "saveParticipants" ], "vs/workbench/services/views/common/viewContainerModel": [ - "views log" + "showViewsLog" ], - "vs/workbench/services/hover/browser/hoverWidget": [ + "vs/editor/browser/services/hoverService/hoverWidget": [ "hoverhint" ], + "vs/editor/browser/services/hoverService/updatableHoverWidget": [ + "iconLabel.loading" + ], "vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl": [ "alreadyDebugging", "stop", @@ -8889,6 +9176,12 @@ "invalid.tokenTypes", "invalid.path.1" ], + "vs/workbench/contrib/preferences/browser/keybindingWidgets": [ + "defineKeybinding.initial", + "defineKeybinding.oneExists", + "defineKeybinding.existing", + "defineKeybinding.chordsTo" + ], "vs/editor/contrib/suggest/browser/suggest": [ "suggestWidgetHasSelection", "suggestWidgetDetailsVisible", @@ -8899,6 +9192,11 @@ "suggestionInsertMode", "suggestionCanResolve" ], + "vs/workbench/contrib/preferences/browser/preferencesActions": [ + "languageDescriptionConfigured", + "pickLanguage", + "configureLanguageBasedSettings" + ], "vs/workbench/contrib/preferences/browser/keybindingsEditor": [ "recordKeysLabel", "sortByPrecedeneLabel", @@ -8934,11 +9232,6 @@ "noWhen", "keyboard shortcuts aria label" ], - "vs/workbench/contrib/preferences/browser/preferencesActions": [ - "configureLanguageBasedSettings", - "languageDescriptionConfigured", - "pickLanguage" - ], "vs/workbench/contrib/preferences/browser/preferencesIcons": [ "settingsScopeDropDownIcon", "settingsMoreActionIcon", @@ -8953,6 +9246,13 @@ "settingsFilter", "preferencesOpenSettings" ], + "vs/workbench/contrib/preferences/common/preferencesContribution": [ + "splitSettingsEditorLabel", + "enableNaturalLanguageSettingsSearch", + "settingsSearchTocBehavior.hide", + "settingsSearchTocBehavior.filter", + "settingsSearchTocBehavior" + ], "vs/workbench/contrib/preferences/browser/settingsEditor2": [ "SearchSettings.AriaLabel", "clearInput", @@ -8967,84 +9267,43 @@ "turnOnSyncButton", "lastSyncedLabel" ], - "vs/workbench/contrib/preferences/common/preferencesContribution": [ - "splitSettingsEditorLabel", - "enableNaturalLanguageSettingsSearch", - "settingsSearchTocBehavior.hide", - "settingsSearchTocBehavior.filter", - "settingsSearchTocBehavior" - ], - "vs/workbench/contrib/preferences/browser/keybindingWidgets": [ - "defineKeybinding.initial", - "defineKeybinding.oneExists", - "defineKeybinding.existing", - "defineKeybinding.chordsTo" - ], "vs/workbench/contrib/performance/browser/perfviewEditor": [ "name" ], - "vs/workbench/contrib/notebook/browser/notebookEditor": [ - "fail.noEditor", - "fail.noEditor.extensionMissing", - "notebookOpenEnableMissingViewType", - "notebookOpenInstallMissingViewType", - "notebookOpenAsText", - "notebookOpenInTextEditor" - ], - "vs/workbench/contrib/notebook/common/notebookEditorInput": [ - "vetoExtHostRestart" - ], - "vs/workbench/contrib/notebook/browser/services/notebookServiceImpl": [ - "notebookOpenInstallMissingViewType" - ], - "vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor": [ - "notebookTreeAriaLabel" - ], - "vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl": [ - "disableOtherKeymapsConfirmation", - "yes", - "no" - ], - "vs/workbench/contrib/notebook/browser/services/notebookExecutionServiceImpl": [ - "notebookRunTrust" - ], - "vs/editor/common/languages/modesRegistry": [ - "plainText.alias" - ], - "vs/workbench/contrib/comments/browser/commentReply": [ - "reply", - "newComment", - "reply", - "reply" - ], - "vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl": [ - "workbench.notebook.clearNotebookKernelsMRUCache" - ], - "vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl": [ - "renderChannelName" - ], - "vs/workbench/contrib/notebook/browser/notebookAccessibility": [ - "notebook.overview", - "notebook.cell.edit", - "notebook.cell.editNoKb", - "notebook.cell.quitEdit", - "notebook.cell.quitEditNoKb", - "notebook.cell.focusInOutput", - "notebook.cell.focusInOutputNoKb", - "notebook.cellNavigation", - "notebook.cell.executeAndFocusContainer", - "notebook.cell.executeAndFocusContainerNoKb", - "notebook.cell.insertCodeCellBelowAndFocusContainer", - "notebook.changeCellType" - ], - "vs/workbench/contrib/accessibility/browser/accessibleViewActions": [ - "editor.action.accessibleViewNext", - "editor.action.accessibleViewPrevious", - "editor.action.accessibleViewGoToSymbol", - "editor.action.accessibilityHelp", - "editor.action.accessibleView", - "editor.action.accessibleViewDisableHint", - "editor.action.accessibleViewAcceptInlineCompletionAction" + "vs/workbench/contrib/speech/common/speechService": [ + "hasSpeechProvider", + "speechToTextInProgress", + "speechLanguage.da-DK", + "speechLanguage.de-DE", + "speechLanguage.en-AU", + "speechLanguage.en-CA", + "speechLanguage.en-GB", + "speechLanguage.en-IE", + "speechLanguage.en-IN", + "speechLanguage.en-NZ", + "speechLanguage.en-US", + "speechLanguage.es-ES", + "speechLanguage.es-MX", + "speechLanguage.fr-CA", + "speechLanguage.fr-FR", + "speechLanguage.hi-IN", + "speechLanguage.it-IT", + "speechLanguage.ja-JP", + "speechLanguage.ko-KR", + "speechLanguage.nl-NL", + "speechLanguage.pt-PT", + "speechLanguage.pt-BR", + "speechLanguage.ru-RU", + "speechLanguage.sv-SE", + "speechLanguage.tr-TR", + "speechLanguage.zh-CN", + "speechLanguage.zh-HK", + "speechLanguage.zh-TW" + ], + "vs/workbench/contrib/speech/browser/speechService": [ + "vscode.extension.contributes.speechProvider", + "speechProviderName", + "speechProviderDescription" ], "vs/workbench/contrib/accessibility/browser/accessibleView": [ "symbolLabel", @@ -9062,6 +9321,12 @@ "accessibleViewToolbar", "toolbar", "intro", + "insertAtCursor", + "insertAtCursorNoKb", + "insertIntoNewFile", + "insertIntoNewFileNoKb", + "runInTerminal", + "runInTerminalNoKb", "accessibleViewNextPreviousHint", "chatAccessibleViewNextPreviousHintNoKb", "acessibleViewDisableHint", @@ -9073,11 +9338,309 @@ "accessibleViewSymbolQuickPickPlaceholder", "accessibleViewSymbolQuickPickTitle" ], + "vs/workbench/contrib/accessibility/browser/accessibleViewContributions": [ + "notification.accessibleViewSrc", + "notification.accessibleView", + "clearNotification", + "clearNotification" + ], + "vs/workbench/contrib/accessibility/browser/accessibleViewActions": [ + "editor.action.accessibleViewNext", + "editor.action.accessibleViewNextCodeBlock", + "editor.action.accessibleViewPreviousCodeBlock", + "editor.action.accessibleViewPrevious", + "editor.action.accessibleViewGoToSymbol", + "editor.action.accessibilityHelp", + "editor.action.accessibleView", + "editor.action.accessibleViewDisableHint", + "editor.action.accessibleViewAcceptInlineCompletionAction" + ], + "vs/workbench/contrib/chat/browser/actions/chatClearActions": [ + "chat.newChat.label", + "chat.newChat.label" + ], + "vs/workbench/contrib/chat/browser/actions/chatActions": [ + "interactiveSession.history.delete", + "interactiveSession.history.pick", + "chat.category", + "openChat", + "chat.history.label", + "interactiveSession.open", + "interactiveSession.clearHistory.label", + "chat.clear.label", + "actions.interactiveSession.focus", + "interactiveSession.focusInput.label" + ], + "vs/workbench/contrib/chat/browser/actions/chatCodeblockActions": [ + "interactive.copyCodeBlock.label", + "interactive.insertCodeBlock.label", + "interactive.insertIntoNewFile.label", + "interactive.runInTerminal.label", + "interactive.nextCodeBlock.label", + "interactive.previousCodeBlock.label", + "interactive.compare.apply" + ], + "vs/workbench/contrib/chat/browser/actions/chatCopyActions": [ + "interactive.copyAll.label", + "interactive.copyItem.label" + ], + "vs/workbench/contrib/chat/browser/actions/chatExecuteActions": [ + "interactive.submit.label", + { + "key": "actions.chat.submitSecondaryAgent", + "comment": [ + "Send input from the chat input box to the secondary agent" + ] + }, + "chat.newChat.label", + "interactive.cancel.label" + ], + "vs/workbench/contrib/chat/browser/actions/chatFileTreeActions": [ + "interactive.nextFileTree.label", + "interactive.previousFileTree.label" + ], + "vs/workbench/contrib/chat/browser/actions/chatImportExport": [ + "chat.file.label", + "chat.export.label", + "chat.import.label" + ], + "vs/workbench/contrib/chat/browser/actions/chatMoveActions": [ + "chat.openInEditor.label", + "chat.openInNewWindow.label", + "interactiveSession.openInSidebar.label" + ], + "vs/workbench/contrib/chat/browser/actions/chatQuickInputActions": [ + "toggle.desc", + "toggle.query", + "toggle.isPartialQuery", + "toggle.query", + "chat.openInChatView.label", + "chat.closeQuickChat.label", + "chat.launchInlineChat.label", + "quickChat", + "interactiveSession.open" + ], + "vs/workbench/contrib/chat/browser/actions/chatTitleActions": [ + "reunmenu", + "interactive.helpful.label", + "interactive.unhelpful.label", + "interactive.reportIssueForBug.label", + "interactive.insertIntoNotebook.label", + "chat.remove.label", + "chat.rerun.label", + "chat.rerunWithoutCommandDetection.label" + ], + "vs/workbench/contrib/chat/browser/chat": [ + "generating" + ], + "vs/workbench/contrib/chat/browser/chatEditorInput": [ + "chatEditorLabelIcon", + "chatEditorName" + ], + "vs/workbench/contrib/chat/common/chatContextKeys": [ + "interactiveSessionResponseVote", + "chatSessionResponseDetectedAgentOrCommand", + "chatResponseSupportsIssueReporting", + "chatResponseFiltered", + "interactiveSessionRequestInProgress", + "chatResponse", + "chatRequest", + "chatEditApplied", + "interactiveInputHasText", + "interactiveInputHasFocus", + "inInteractiveInput", + "inChat", + "chatIsEnabled" + ], + "vs/workbench/contrib/chat/common/chatServiceImpl": [ + "chatFailErrorMessage", + "action.showExtension", + "emptyResponse" + ], + "vs/workbench/contrib/chat/common/languageModelStats": [ + "Language Models", + "languageModels" + ], + "vs/workbench/contrib/chat/browser/chatParticipantContributions": [ + "vscode.extension.contributes.chatParticipant", + "chatParticipantId", + "chatParticipantName", + "chatParticipantDescription", + "chatParticipantIsDefaultDescription", + "chatCommandSticky", + "chatCommandsDescription", + "chatCommand", + "chatCommandDescription", + "chatCommandWhen", + "chatCommandSampleRequest", + "chatCommandSticky", + "defaultImplicitVariables", + "chatLocationsDescription", + "chat.viewContainer.label" + ], + "vs/workbench/contrib/chat/common/chatColors": [ + "chat.requestBorder", + "chat.requestBackground", + "chat.slashCommandBackground", + "chat.slashCommandForeground", + "chat.avatarBackground", + "chat.avatarForeground" + ], + "vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib": [ + "pickFileLabel" + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatController": [ + "create.fail", + "welcome.2", + "welcome.1", + "empty", + "err.apply", + "err.discard", + "savehint" + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatActions": [ + "startInlineChat", + "arrowUp", + "arrowDown", + "discard", + "discardMenu", + "discard", + "undo.clipboard", + "undo.newfile", + "apply2", + "cancel", + "close", + "configure", + "label", + "viewInChat", + "run", + "unstash", + "cat", + "focus", + "showChanges", + "apply1", + "moveToNextHunk", + "moveToPreviousHunk", + "copyRecordings" + ], + "vs/workbench/contrib/inlineChat/common/inlineChat": [ + "inlineChatHasProvider", + "inlineChatVisible", + "inlineChatFocused", + "inlineChatResponseFocused", + "inlineChatEmpty", + "inlineChatInnerCursorFirst", + "inlineChatInnerCursorLast", + "inlineChatInnerCursorStart", + "inlineChatInnerCursorEnd", + "inlineChatOuterCursorPosition", + "inlineChatHasActiveRequest", + "inlineChatHasStashedSession", + "inlineChatResponseType", + "inlineChatResponseTypes", + "inlineChatDidEdit", + "inlineChatUserDidEdit", + "inlineChatLastFeedbackKind", + "inlineChatSupportIssueReporting", + "inlineChatDocumentChanged", + "inlineChatChangeHasDiff", + "inlineChatChangeShowsDiff", + "inlineChat.background", + "inlineChat.border", + "inlineChat.shadow", + "inlineChat.regionHighlight", + "inlineChatInput.border", + "inlineChatInput.focusBorder", + "inlineChatInput.placeholderForeground", + "inlineChatInput.background", + "inlineChatDiff.inserted", + "editorOverviewRuler.inlineChatInserted", + "editorOverviewRuler.inlineChatInserted", + "inlineChatDiff.removed", + "editorOverviewRuler.inlineChatRemoved", + "mode", + "mode.live", + "mode.preview", + "finishOnType", + "acceptedOrDiscardBeforeSave", + "holdToSpeech", + "accessibleDiffView", + "accessibleDiffView.auto", + "accessibleDiffView.on", + "accessibleDiffView.off" + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl": [ + "inlineChat", + "inlineChat.N" + ], + "vs/workbench/contrib/notebook/browser/notebookEditor": [ + "fail.noEditor", + "fail.noEditor.extensionMissing", + "notebookOpenEnableMissingViewType", + "notebookOpenInstallMissingViewType", + "notebookOpenAsText", + "notebookTooLargeForHeapErrorWithSize", + "notebookTooLargeForHeapErrorWithoutSize", + "notebookOpenInTextEditor" + ], + "vs/workbench/contrib/notebook/common/notebookEditorInput": [ + "vetoExtHostRestart" + ], + "vs/workbench/contrib/notebook/browser/services/notebookServiceImpl": [ + "notebookOpenInstallMissingViewType" + ], + "vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor": [ + "notebookTreeAriaLabel" + ], + "vs/workbench/contrib/notebook/browser/services/notebookExecutionServiceImpl": [ + "notebookRunTrust" + ], + "vs/editor/common/languages/modesRegistry": [ + "plainText.alias" + ], + "vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl": [ + "disableOtherKeymapsConfirmation", + "yes", + "no" + ], + "vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl": [ + "workbench.notebook.clearNotebookKernelsMRUCache" + ], + "vs/workbench/contrib/comments/browser/commentReply": [ + "reply", + "newComment", + "reply", + "reply" + ], + "vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl": [ + "renderChannelName" + ], + "vs/workbench/contrib/notebook/browser/notebookAccessibility": [ + "notebook.overview", + "notebook.cell.edit", + "notebook.cell.editNoKb", + "notebook.cell.quitEdit", + "notebook.cell.quitEditNoKb", + "notebook.cell.focusInOutput", + "notebook.cell.focusInOutputNoKb", + "notebook.focusNextEditor", + "notebook.focusNextEditorNoKb", + "notebook.focusPreviousEditor", + "notebook.focusPreviousEditorNoKb", + "notebook.cellNavigation", + "notebook.cell.executeAndFocusContainer", + "notebook.cell.executeAndFocusContainerNoKb", + "notebook.cell.insertCodeCellBelowAndFocusContainer", + "notebook.changeCellType" + ], + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariables": [ + "notebookVariables" + ], "vs/workbench/contrib/notebook/browser/controller/coreActions": [ - "notebookActions.category", "notebookMenu.insertCell", "notebookMenu.cellTitle", - "miShare" + "miShare", + "notebookActions.category" ], "vs/workbench/contrib/notebook/browser/controller/insertCellActions": [ "notebookActions.insertCodeCellAbove", @@ -9105,46 +9668,92 @@ "notebookActions.menu.insertMarkdown", "notebookActions.menu.insertMarkdown.tooltip" ], - "vs/workbench/contrib/notebook/browser/controller/executeActions": [ - "notebookActions.renderMarkdown", - "notebookActions.executeNotebook", - "notebookActions.executeNotebook", - "notebookActions.execute", - "notebookActions.execute", - "notebookActions.executeAbove", - "notebookActions.executeBelow", - "notebookActions.executeAndFocusContainer", - "notebookActions.executeAndFocusContainer", - "notebookActions.cancel", - "notebookActions.cancel", - "notebookActions.executeAndSelectBelow", + "vs/workbench/contrib/notebook/browser/controller/sectionActions": [ + { + "key": "mirunCell", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "runCell", + { + "key": "mirunCellsInSection", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "runCellsInSection", + { + "key": "mifoldSection", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "foldSection", + { + "key": "miexpandSection", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "expandSection", + "runCell", + "runCellsInSection", + "foldSection", + "expandSection" + ], + "vs/workbench/contrib/notebook/browser/controller/executeActions": [ + "notebookActions.renderMarkdown", + "notebookActions.executeNotebook", + "notebookActions.executeNotebook", + "notebookActions.execute", + "notebookActions.execute", + "notebookActions.executeAbove", + "notebookActions.executeBelow", + "notebookActions.executeAndFocusContainer", + "notebookActions.executeAndFocusContainer", + "notebookActions.cancel", + "notebookActions.cancel", + "notebookActions.executeAndSelectBelow", "notebookActions.executeAndInsertBelow", - "notebookActions.cancelNotebook", - "notebookActions.interruptNotebook", + "revealRunningCellShort", + "revealRunningCell", "revealRunningCell", "revealRunningCell", - "revealRunningCellShort", "revealLastFailedCell", "revealLastFailedCell", - "revealLastFailedCellShort" - ], - "vs/workbench/contrib/notebook/browser/controller/cellOutputActions": [ - "notebookActions.copyOutput" + "revealLastFailedCellShort", + "notebookActions.cancelNotebook", + "notebookActions.interruptNotebook" ], "vs/workbench/contrib/notebook/browser/controller/layoutActions": [ + "notebook.showLineNumbers", + "notebook.placeholder", + "saveTarget.machine", + "saveTarget.workspace", + { + "key": "mitoggleNotebookStickyScroll", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "notebookStickyScroll", + { + "key": "mitoggleNotebookStickyScroll", + "comment": [ + "&& denotes a mnemonic" + ] + }, "workbench.notebook.layout.select.label", "workbench.notebook.layout.configure.label", "workbench.notebook.layout.configure.label", "customizeNotebook", "notebook.toggleLineNumbers", - "notebook.showLineNumbers", "notebook.toggleCellToolbarPosition", "notebook.toggleBreadcrumb", "notebook.saveMimeTypeOrder", - "notebook.placeholder", - "saveTarget.machine", - "saveTarget.workspace", - "workbench.notebook.layout.webview.reset.label" + "workbench.notebook.layout.webview.reset.label", + "toggleStickyScroll" ], "vs/workbench/contrib/notebook/browser/controller/editActions": [ "notebookActions.editCell", @@ -9162,50 +9771,40 @@ "autoDetect", "languagesPicks", "pickLanguageToConfigure", + "noDetection", + "noNotebookEditor", + "noWritableCodeEditor", + "indentConvert", + "indentView", + "pickAction", "detectLanguage", - "noDetection" + "selectNotebookIndentation" + ], + "vs/workbench/contrib/notebook/browser/controller/cellOutputActions": [ + "notebookActions.copyOutput" ], "vs/workbench/contrib/notebook/browser/controller/foldingController": [ "fold.cell", "unfold.cell", "fold.cell" ], - "vs/workbench/contrib/notebook/browser/contrib/format/formatting": [ - "format.title", - "label", - "formatCell.label", - "formatCells.label" - ], - "vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted": [ - "workbench.notebook.layout.gettingStarted.label" - ], - "vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions": [ - "notebook.toggleCellToolbarPosition" + "vs/workbench/contrib/notebook/browser/contrib/find/notebookFind": [ + "notebookActions.hideFind", + "notebookActions.findInNotebook" ], "vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard": [ "notebookActions.copy", "notebookActions.cut", "notebookActions.paste", "notebookActions.pasteAbove", + "notebook.cell.output.selectAll", "toggleNotebookClipboardLog" ], - "vs/workbench/contrib/notebook/browser/contrib/find/notebookFind": [ - "notebookActions.hideFind", - "notebookActions.findInNotebook" - ], - "vs/workbench/contrib/notebook/browser/contrib/navigation/arrow": [ - "cursorMoveDown", - "cursorMoveUp", - "focusFirstCell", - "focusLastCell", - "focusOutput", - "focusOutputOut", - "notebookActions.centerActiveCell", - "cursorPageUp", - "cursorPageUpSelect", - "cursorPageDown", - "cursorPageDownSelect", - "notebook.navigation.allowNavigateToSurroundingCells" + "vs/workbench/contrib/notebook/browser/contrib/format/formatting": [ + "label", + "formatCell.label", + "formatCells.label", + "format.title" ], "vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants": [ "notebookFormatSave.formatting", @@ -9223,10 +9822,37 @@ }, "codeAction.apply" ], + "vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted": [ + "workbench.notebook.layout.gettingStarted.label" + ], + "vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions": [ + "notebook.toggleCellToolbarPosition" + ], "vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline": [ + "outline.showMarkdownHeadersOnly", "outline.showCodeCells", + "outline.showCodeCellSymbols", "breadcrumbs.showCodeCells", - "notebook.gotoSymbols.showAllSymbols" + "notebook.gotoSymbols.showAllSymbols", + "filter", + "toggleShowMarkdownHeadersOnly", + "toggleCodeCells", + "toggleCodeCellSymbols" + ], + "vs/workbench/contrib/notebook/browser/contrib/navigation/arrow": [ + "notebook.cell.webviewHandledEvents", + "cursorMoveDown", + "cursorMoveUp", + "focusFirstCell", + "focusLastCell", + "focusOutput", + "focusOutputOut", + "notebookActions.centerActiveCell", + "cursorPageUp", + "cursorPageUpSelect", + "cursorPageDown", + "cursorPageDownSelect", + "notebook.navigation.allowNavigateToSurroundingCells" ], "vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile": [ "setProfileTitle" @@ -9242,7 +9868,8 @@ "notebook.cell.status.pending", "notebook.cell.status.executing", "notebook.cell.statusBar.timerTooltip.reportIssueFootnote", - "notebook.cell.statusBar.timerTooltip" + "notebook.cell.statusBar.timerTooltip", + "notebook.cell.status.diagnostic" ], "vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar": [ "notebook.info", @@ -9252,7 +9879,9 @@ "kernel.select.label", "notebook.activeCellStatusName", "notebook.multiActiveCellIndicator", - "notebook.singleActiveCellIndicator" + "notebook.singleActiveCellIndicator", + "notebook.indentation", + "selectNotebookIndentation" ], "vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout": [ "workbench.notebook.toggleLayoutTroubleshoot", @@ -9260,6 +9889,8 @@ "workbench.notebook.clearNotebookEdtitorTypeCache" ], "vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands": [ + "notebookActions.toggleOutputs", + "cellCommands.quickFix.noneMessage", "notebookActions.moveCellUp", "notebookActions.moveCellDown", "notebookActions.copyCellUp", @@ -9275,246 +9906,131 @@ "notebookActions.collapseCellOutput", "notebookActions.expandCellOutput", "notebookActions.toggleOutputs", - "notebookActions.toggleOutputs", "notebookActions.collapseAllCellInput", "notebookActions.expandAllCellInput", "notebookActions.collapseAllCellOutput", "notebookActions.expandAllCellOutput", - "notebookActions.toggleScrolling" + "notebookActions.toggleScrolling", + "notebookActions.cellFailureActions" ], "vs/workbench/contrib/notebook/browser/diff/notebookDiffActions": [ - "notebook.diff.switchToText", "notebook.diff.cell.revertMetadata", "notebook.diff.cell.switchOutputRenderingStyleToText", "notebook.diff.cell.revertOutputs", "notebook.diff.cell.revertInput", - "notebook.diff.showOutputs", - "notebook.diff.showMetadata", "notebook.diff.action.previous.title", "notebook.diff.action.next.title", "notebook.diff.ignoreMetadata", - "notebook.diff.ignoreOutputs" - ], - "vs/workbench/contrib/chat/browser/actions/chatActions": [ - "chat.category", - "quickChat", - { - "key": "actions.chat.acceptInput", - "comment": [ - "Apply input from the chat input box" - ] - }, - "interactiveSession.clearHistory.label", - "actions.interactiveSession.focus", - "interactiveSession.focusInput.label", - "interactiveSession.open", - "interactiveSession.history.label", - "interactiveSession.history.delete", - "interactiveSession.history.pick" - ], - "vs/workbench/contrib/chat/browser/actions/chatCopyActions": [ - "interactive.copyAll.label", - "interactive.copyItem.label" - ], - "vs/workbench/contrib/chat/browser/actions/chatExecuteActions": [ - "interactive.submit.label", - "interactive.cancel.label" - ], - "vs/workbench/contrib/chat/browser/actions/chatTitleActions": [ - "interactive.helpful.label", - "interactive.unhelpful.label", - "interactive.insertIntoNotebook.label", - "chat.remove.label" + "notebook.diff.ignoreOutputs", + "notebook.diff.switchToText", + "notebook.diff.showOutputs", + "notebook.diff.showMetadata" ], - "vs/workbench/contrib/chat/browser/actions/chatQuickInputActions": [ - "chat.openInChatView.label", - "chat.closeQuickChat.label", - "quickChat", - "interactiveSession.open" + "vs/editor/contrib/peekView/browser/peekView": [ + "inReferenceSearchEditor", + "label.close", + "peekViewTitleBackground", + "peekViewTitleForeground", + "peekViewTitleInfoForeground", + "peekViewBorder", + "peekViewResultsBackground", + "peekViewResultsMatchForeground", + "peekViewResultsFileForeground", + "peekViewResultsSelectionBackground", + "peekViewResultsSelectionForeground", + "peekViewEditorBackground", + "peekViewEditorGutterBackground", + "peekViewEditorStickScrollBackground", + "peekViewResultsMatchHighlight", + "peekViewEditorMatchHighlight", + "peekViewEditorMatchHighlightBorder" ], - "vs/workbench/contrib/chat/browser/actions/chatCodeblockActions": [ - "interactive.copyCodeBlock.label", - "interactive.insertCodeBlock.label", - "interactive.insertIntoNewFile.label", - "interactive.runInTerminal.label", - "interactive.nextCodeBlock.label", - "interactive.previousCodeBlock.label" + "vs/workbench/contrib/interactive/browser/interactiveEditor": [ + "interactiveInputPlaceHolder" ], - "vs/workbench/contrib/chat/browser/actions/chatImportExport": [ - "chat.file.label", - "chat.export.label", - "chat.import.label" + "vs/workbench/contrib/notebook/browser/notebookIcons": [ + "selectKernelIcon", + "executeIcon", + "executeAboveIcon", + "executeBelowIcon", + "stopIcon", + "deleteCellIcon", + "executeAllIcon", + "editIcon", + "stopEditIcon", + "moveUpIcon", + "moveDownIcon", + "clearIcon", + "splitCellIcon", + "successStateIcon", + "errorStateIcon", + "pendingStateIcon", + "executingStateIcon", + "collapsedIcon", + "expandedIcon", + "openAsTextIcon", + "revertIcon", + "renderOutputIcon", + "mimetypeIcon", + "copyIcon", + "previousChangeIcon", + "nextChangeIcon", + "variablesViewIcon" ], - "vs/workbench/contrib/chat/browser/chatContributionServiceImpl": [ - "vscode.extension.contributes.interactiveSession", - "vscode.extension.contributes.interactiveSession.id", - "vscode.extension.contributes.interactiveSession.label", - "vscode.extension.contributes.interactiveSession.icon", - "vscode.extension.contributes.interactiveSession.when", - "chat.viewContainer.label" + "vs/platform/quickinput/browser/helpQuickAccess": [ + "helpPickAriaLabel" ], - "vs/workbench/contrib/chat/browser/chatEditorInput": [ - "chatEditorName" + "vs/workbench/contrib/quickaccess/browser/viewQuickAccess": [ + "noViewResults", + "views", + "panels", + "secondary side bar", + "terminalTitle", + "terminals", + "debugConsoles", + "channels", + "openView", + "quickOpenView" ], - "vs/workbench/contrib/chat/common/chatServiceImpl": [ - "emptyResponse" + "vs/workbench/contrib/files/browser/fileConstants": [ + "removeFolderFromWorkspace", + "saveAs", + "save", + "saveWithoutFormatting", + "saveAll", + "newUntitledFile" ], - "vs/workbench/contrib/chat/browser/actions/chatMoveActions": [ - "chat.openInEditor.label", - "interactiveSession.openInEditor.label", - "interactiveSession.openInSidebar.label" - ], - "vs/workbench/contrib/chat/common/chatContextKeys": [ - "interactiveSessionResponseHasProviderId", - "interactiveSessionResponseVote", - "chatResponseFiltered", - "interactiveSessionRequestInProgress", - "chatResponse", - "chatRequest", - "interactiveInputHasText", - "inInteractiveInput", - "inChat", - "hasChatProvider" - ], - "vs/workbench/contrib/chat/browser/actions/chatClearActions": [ - "interactiveSession.clear.label", - "interactiveSession.clear.label", - "interactiveSession.clear.label" - ], - "vs/workbench/contrib/chat/common/chatViewModel": [ - "thinking" - ], - "vs/workbench/contrib/chat/common/chatSlashCommands": [ - "command", - "details", - "vscode.extension.contributes.slashes", - "invalid" - ], - "vs/workbench/contrib/chat/browser/actions/chatFileTreeActions": [ - "interactive.nextFileTree.label", - "interactive.previousFileTree.label" - ], - "vs/workbench/contrib/accessibility/browser/accessibilityContributions": [ - "notification.accessibleViewSrc", - "notification.accessibleView", - "clearNotification", - "clearNotification" - ], - "vs/workbench/contrib/chat/common/chatAgents": [ - "agent", - "details", - "vscode.extension.contributes.slashes", - "invalid" - ], - "vs/workbench/contrib/chat/common/chatColors": [ - "chat.requestBorder", - "chat.slashCommandBackground", - "chat.slashCommandForeground" - ], - "vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib": [ - "interactive.input.placeholderWithCommands", - "interactive.input.placeholderNoCommands" - ], - "vs/workbench/contrib/inlineChat/browser/inlineChatController": [ - "welcome.1", - "welcome.2", - "create.fail", - "create.fail.detail", - "welcome.1", - "default.placeholder", - "default.placeholder.history", - "thinking", - "empty", - "markdownResponseMessage", - "editResponseMessage", - "err.apply", - "err.discard" - ], - "vs/workbench/contrib/inlineChat/common/inlineChat": [ - "inlineChatHasProvider", - "inlineChatVisible", - "inlineChatFocused", - "inlineChatResponseFocused", - "inlineChatEmpty", - "inlineChatInnerCursorFirst", - "inlineChatInnerCursorLast", - "inlineChatInnerCursorStart", - "inlineChatInnerCursorEnd", - "inlineChatMarkdownMessageCropState", - "inlineChatOuterCursorPosition", - "inlineChatHasActiveRequest", - "inlineChatHasStashedSession", - "inlineChatResponseType", - "inlineChatResponseTypes", - "inlineChatDidEdit", - "inlineChatUserDidEdit", - "inlineChatLastFeedbackKind", - "inlineChatDocumentChanged", - "inlineChat.background", - "inlineChat.border", - "inlineChat.shadow", - "inlineChat.regionHighlight", - "inlineChatInput.border", - "inlineChatInput.focusBorder", - "inlineChatInput.placeholderForeground", - "inlineChatInput.background", - "inlineChatDiff.inserted", - "inlineChatDiff.removed", - "mode", - "mode.livePreview", - "mode.preview", - "mode.live", - "showDiff" - ], - "vs/workbench/contrib/inlineChat/browser/inlineChatActions": [ - "run", - "unstash", - "cat", - "accept", - "rerun", - "rerunShort", - "stop", - "arrowUp", - "arrowDown", - "focus", - "previousFromHistory", - "nextFromHistory", - "discardMenu", - "discard", - "undo.clipboard", - "undo.newfile", - "feedback.helpful", - "feedback.unhelpful", - "showDiff", - { - "key": "miShowDiff", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "showDiff2", - { - "key": "miShowDiff2", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "apply1", - "apply2", - "cancel", - "copyRecordings", - "label", - "viewInChat", - "expandMessage", - "contractMessage" - ], - "vs/workbench/contrib/files/browser/fileConstants": [ - "saveAs", - "save", - "saveWithoutFormatting", - "saveAll", - "removeFolderFromWorkspace", - "newUntitledFile" + "vs/workbench/contrib/quickaccess/browser/commandsQuickAccess": [ + "noCommandResults", + "configure keybinding", + "askXInChat", + "commandWithCategory", + "confirmClearMessage", + "confirmClearDetail", + { + "key": "clearButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "showTriggerActions", + "clearCommandHistory" + ], + "vs/workbench/contrib/testing/browser/codeCoverageDecorations": [ + "testing.toggleInlineCoverage", + "coverage.branches", + "coverage.branchNotCovered", + "coverage.branchCoveredYes", + "coverage.branchCovered", + "coverage.declExecutedNo", + "coverage.declExecutedCount", + "coverage.declExecutedYes", + "coverage.toggleInline" + ], + "vs/workbench/contrib/testing/browser/testCoverageBars": [ + "statementCoverage", + "functionCoverage", + "branchCoverage" ], "vs/workbench/contrib/testing/browser/icons": [ "testViewIcon", @@ -9524,6 +10040,8 @@ "testingRunAllIcon", "testingDebugAllIcon", "testingDebugIcon", + "testingCoverageIcon", + "testingRunAllWithCoverageIcon", "testingCancelIcon", "filterIcon", "hiddenIcon", @@ -9535,6 +10053,9 @@ "testingTurnContinuousRunOff", "testingTurnContinuousRunIsOn", "testingCancelRefreshTests", + "testingCoverage", + "testingWasCovered", + "testingMissingBranch", "testingErrorIcon", "testingFailedIcon", "testingPassedIcon", @@ -9542,20 +10063,39 @@ "testingSkippedIcon", "testingUnsetIcon" ], + "vs/workbench/contrib/testing/browser/testCoverageView": [ + "functionsWithoutCoverage", + "loadingCoverageDetails", + "testCoverageItemLabel", + "testCoverageTreeLabel", + "testing.coverageSortByLocation", + "testing.coverageSortByLocationDescription", + "testing.coverageSortByCoverage", + "testing.coverageSortByCoverageDescription", + "testing.coverageSortByName", + "testing.coverageSortByNameDescription", + "testing.coverageSortPlaceholder", + "testing.changeCoverageSort" + ], "vs/workbench/contrib/testing/browser/testingDecorations": [ "peekTestOutout", "expected.title", "actual.title", "testing.gutterMsg.contextMenu", "testing.gutterMsg.debug", + "testing.gutterMsg.coverage", "testing.gutterMsg.run", "run test", "debug test", + "coverage test", "testing.runUsing", "peek failure", "reveal test", "run all test", - "debug all test" + "run all test with coverage", + "debug all test", + "testOverflowItems", + "selectTestToRun" ], "vs/workbench/contrib/testing/browser/testingProgressUiService": [ "testProgress.runningInitial", @@ -9610,6 +10150,7 @@ "testing.defaultGutterClickAction", "testing.defaultGutterClickAction.run", "testing.defaultGutterClickAction.debug", + "testing.defaultGutterClickAction.coverage", "testing.defaultGutterClickAction.contextMenu", "testing.gutterEnabled", "testing.saveBeforeTest", @@ -9618,16 +10159,24 @@ "testing.openTesting.openOnTestFailure", "testing.openTesting.openExplorerOnTestStart", "testing.openTesting", - "testing.alwaysRevealTestOnStateChange" + "testing.alwaysRevealTestOnStateChange", + "testing.ShowCoverageInExplorer", + "testing.displayedCoveragePercent", + "testing.displayedCoveragePercent.totalCoverage", + "testing.displayedCoveragePercent.statement", + "testing.displayedCoveragePercent.minimum", + "testing.coverageBarThresholds" ], "vs/workbench/contrib/testing/browser/testingOutputPeek": [ "testing.markdownPeekError", "testOutputTitle", "testingOutputExpected", "testingOutputActual", - "runNoOutputForPast", + "caseNoOutput", "runNoOutput", - "close", + "runNoOutputForPast", + "openTestCoverage", + "closeTestCoverage", "testUnnamedTask", "messageMoreLinesN", "messageMoreLines1", @@ -9642,10 +10191,14 @@ "debug test", "testing.goToFile", "testing.goToError", + "close", "testing.goToNextMessage", + "testing.goToNextMessage.description", "testing.goToPreviousMessage", + "testing.goToPreviousMessage.description", "testing.openMessageInEditor", - "testing.toggleTestingPeekHistory" + "testing.toggleTestingPeekHistory", + "testing.toggleTestingPeekHistory.description" ], "vs/workbench/contrib/testing/common/testingContentProvider": [ "runNoOutout" @@ -9668,65 +10221,54 @@ "testing.supportsContinuousRun", "testing.isParentRunningContinuously", "testing.activeEditorHasTests", + "testing.isTestCoverageOpen", "testing.peekItemType", "testing.controllerId", "testing.testId", "testing.testItemHasUri", "testing.testItemIsHidden", "testing.testMessage", - "testing.testResultOutdated" + "testing.testResultOutdated", + "testing.testResultState" ], "vs/workbench/contrib/testing/browser/testingConfigurationUi": [ "testConfigurationUi.pick", "updateTestConfiguration" ], - "vs/workbench/contrib/logs/common/logsActions": [ - "setLogLevel", - "all", - "extensionLogs", - "loggers", - "selectlog", - "selectLogLevelFor", - "selectLogLevel", - "resetLogLevel", - "trace", - "debug", - "info", - "warn", - "err", - "off", - "default", - "openSessionLogFile", - "current", - "sessions placeholder", - "log placeholder" - ], "vs/workbench/contrib/testing/browser/testExplorerActions": [ + "testing.toggleContinuousRunOff", + "configureProfile", + "testing.noProfiles", + "testing.selectContinuousProfiles", + "discoveringTests", + "noTestProvider", + "noDebugTestProvider", + "noCoverageTestProvider", + "noTestsAtCursor", + "noTests", + "noTestsInFile", + "testing.noCoverage", + "runSelectedTests", + "debugSelectedTests", + "coverageSelectedTests", "hideTest", "unhideTest", "unhideAllTests", "debug test", + "run with cover test", "testing.runUsing", "run test", "testing.selectDefaultTestProfiles", "testing.toggleContinuousRunOn", - "testing.toggleContinuousRunOff", "testing.startContinuousRunUsing", "testing.configureProfile", - "configureProfile", "testing.stopContinuous", - "testing.noProfiles", - "testing.selectContinuousProfiles", "testing.startContinuous", "getSelectedProfiles", "getExplorerSelection", - "runSelectedTests", - "debugSelectedTests", - "discoveringTests", "runAllTests", - "noTestProvider", "debugAllTests", - "noDebugTestProvider", + "runAllWithCoverage", "testing.cancelRun", "testing.viewAsList", "testing.viewAsTree", @@ -9737,147 +10279,72 @@ "testing.collapseAll", "testing.clearResults", "testing.editFocusedTest", - "noTestsAtCursor", "testing.runAtCursor", "testing.debugAtCursor", - "noTestsInFile", + "testing.coverageAtCursor", "testing.runCurrentFile", "testing.debugCurrentFile", + "testing.coverageCurrentFile", "testing.reRunFailTests", "testing.debugFailTests", "testing.reRunLastRun", "testing.debugLastRun", + "testing.coverageLastRun", "testing.searchForTestExtension", "testing.openOutputPeek", "testing.toggleInlineTestOutput", "testing.refreshTests", - "testing.cancelTestRefresh" - ], - "vs/editor/contrib/peekView/browser/peekView": [ - "inReferenceSearchEditor", - "label.close", - "peekViewTitleBackground", - "peekViewTitleForeground", - "peekViewTitleInfoForeground", - "peekViewBorder", - "peekViewResultsBackground", - "peekViewResultsMatchForeground", - "peekViewResultsFileForeground", - "peekViewResultsSelectionBackground", - "peekViewResultsSelectionForeground", - "peekViewEditorBackground", - "peekViewEditorGutterBackground", - "peekViewEditorStickScrollBackground", - "peekViewResultsMatchHighlight", - "peekViewEditorMatchHighlight", - "peekViewEditorMatchHighlightBorder" - ], - "vs/workbench/contrib/notebook/browser/notebookIcons": [ - "selectKernelIcon", - "executeIcon", - "executeAboveIcon", - "executeBelowIcon", - "stopIcon", - "deleteCellIcon", - "executeAllIcon", - "editIcon", - "stopEditIcon", - "moveUpIcon", - "moveDownIcon", - "clearIcon", - "splitCellIcon", - "successStateIcon", - "errorStateIcon", - "pendingStateIcon", - "executingStateIcon", - "collapsedIcon", - "expandedIcon", - "openAsTextIcon", - "revertIcon", - "renderOutputIcon", - "mimetypeIcon", - "copyIcon", - "previousChangeIcon", - "nextChangeIcon" + "testing.cancelTestRefresh", + "testing.clearCoverage", + "testing.openCoverage" ], - "vs/workbench/contrib/interactive/browser/interactiveEditor": [ - "interactiveInputPlaceHolder" + "vs/workbench/contrib/files/browser/views/emptyView": [ + "noWorkspace" ], - "vs/platform/quickinput/browser/helpQuickAccess": [ - "helpPickAriaLabel" + "vs/workbench/contrib/files/browser/views/explorerView": [ + "explorerSection", + "createNewFile", + "createNewFolder", + "refreshExplorer", + "refreshExplorerMetadata", + "collapseExplorerFolders", + "collapseExplorerFoldersMetadata" ], - "vs/workbench/contrib/quickaccess/browser/commandsQuickAccess": [ - "noCommandResults", - "configure keybinding", - "askXInChat", - "commandWithCategory", - "showTriggerActions", - "clearCommandHistory", - "confirmClearMessage", - "confirmClearDetail", + "vs/workbench/contrib/files/browser/views/openEditorsView": [ + "dirtyCounter", + "openEditors", { - "key": "clearButtonLabel", + "key": "miToggleEditorLayout", "comment": [ "&& denotes a mnemonic" ] - } - ], - "vs/workbench/contrib/quickaccess/browser/viewQuickAccess": [ - "noViewResults", - "views", - "panels", - "secondary side bar", - "terminalTitle", - "terminals", - "debugConsoles", - "channels", - "openView", - "quickOpenView" - ], - "vs/workbench/contrib/files/browser/fileCommands": [ - "modifiedLabel", + }, { - "key": "genericSaveError", + "key": "openEditors", "comment": [ - "{0} is the resource that failed to save and {1} the error message" + "Open is an adjective" ] }, - "retry", - "discard", - "genericRevertError", - "newFileCommand.saveLabel" + "flipLayout", + "miToggleEditorLayoutWithoutMnemonic", + "newUntitledFile" ], - "vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler": [ - "userGuide", - "staleSaveError", - "readonlySaveErrorAdmin", - "readonlySaveErrorSudo", - "readonlySaveError", - "permissionDeniedSaveError", - "permissionDeniedSaveErrorSudo", - { - "key": "genericSaveError", - "comment": [ - "{0} is the resource that failed to save and {1} the error message" - ] - }, - "learnMore", - "dontShowAgain", - "compareChanges", - "saveConflictDiffLabel", - "overwriteElevated", - "overwriteElevatedSudo", - "saveElevated", - "saveElevatedSudo", - "retry", - "discard", - "overwrite", - "overwrite", - "configure" + "vs/workbench/contrib/logs/common/logsActions": [ + "all", + "extensionLogs", + "loggers", + "selectlog", + "selectLogLevelFor", + "selectLogLevel", + "resetLogLevel", + "default", + "current", + "sessions placeholder", + "log placeholder", + "setLogLevel", + "openSessionLogFile" ], "vs/workbench/contrib/files/browser/fileActions": [ - "newFile", - "newFolder", "rename", "delete", "copyFile", @@ -9960,27 +10427,29 @@ "confirmDeleteMessageFile", "confirmOverwrite", "replaceButtonLabel", - "globalCompareFile", - "toggleAutoSave", "saveAllInGroup", "closeGroup", - "focusFilesExplorer", - "showInExplorer", - "openFileInNewWindow", "openFileToShowInNewWindow.unsupportedschema", "emptyFileNameError", "fileNameStartsWithSlashError", "fileNameExistsError", "invalidFileNameError", "fileNameWhitespaceWarning", - "compareNewUntitledTextFiles", - "compareWithClipboard", "clipboardComparisonLabel", "retry", "createBulkEdit", "creatingBulkEdit", "renameBulkEdit", "renamingBulkEdit", + "confirmMultiPasteNative", + "confirmPasteNative", + "doNotAskAgain", + { + "key": "pasteButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, "fileIsAncestor", { "key": "movingBulkEdit", @@ -10006,6 +10475,7 @@ "Placeholder will be replaced by the name of the file moved." ] }, + "fileDeleted", { "key": "copyingBulkEdit", "comment": [ @@ -10030,44 +10500,77 @@ "Placeholder will be replaced by the name of the file copied." ] }, - "fileDeleted", + "newFile", + "newFolder", + "globalCompareFile", + "compareFileWithMeta", + "toggleAutoSave", + "toggleAutoSaveDescription", + "focusFilesExplorer", + "focusFilesExplorerMetadata", + "showInExplorer", + "showInExplorerMetadata", + "openFileInEmptyWorkspace", + "openFileInEmptyWorkspaceMetadata", + "compareNewUntitledTextFiles", + "compareNewUntitledTextFilesMeta", + "compareWithClipboard", + "compareWithClipboardMeta", "setActiveEditorReadonlyInSession", "setActiveEditorWriteableInSession", "toggleActiveEditorReadonlyInSession", "resetActiveEditorReadonlyInSession" ], - "vs/workbench/contrib/files/browser/views/emptyView": [ - "noWorkspace" - ], - "vs/workbench/contrib/files/browser/views/explorerView": [ - "explorerSection", - "createNewFile", - "createNewFolder", - "refreshExplorer", - "collapseExplorerFolders" - ], - "vs/workbench/contrib/files/browser/views/openEditorsView": [ + "vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler": [ + "userGuide", + "staleSaveError", + "readonlySaveErrorAdmin", + "readonlySaveErrorSudo", + "readonlySaveError", + "permissionDeniedSaveError", + "permissionDeniedSaveErrorSudo", { - "key": "openEditors", + "key": "genericSaveError", "comment": [ - "Open is an adjective" + "{0} is the resource that failed to save and {1} the error message" ] }, - "dirtyCounter", - "openEditors", - "flipLayout", - "miToggleEditorLayoutWithoutMnemonic", + "learnMore", + "dontShowAgain", + "compareChanges", + "saveConflictDiffLabel", + "overwriteElevated", + "overwriteElevatedSudo", + "saveElevated", + "saveElevatedSudo", + "retry", + "discard", + "overwrite", + "overwrite", + "configure" + ], + "vs/workbench/contrib/files/browser/fileCommands": [ + "modifiedLabel", { - "key": "miToggleEditorLayout", + "key": "genericSaveError", "comment": [ - "&& denotes a mnemonic" + "{0} is the resource that failed to save and {1} the error message" ] }, - "newUntitledFile" + "retry", + "discard", + "genericRevertError", + "newFileCommand.saveLabel" ], "vs/workbench/contrib/files/browser/editors/binaryFileEditor": [ "binaryFileEditor" ], + "vs/workbench/contrib/files/browser/workspaceWatcher": [ + "enospcError", + "learnMore", + "eshutdownError", + "reload" + ], "vs/editor/common/config/editorConfigurationSchema": [ "editorConfigurationTitle", "tabSize", @@ -10076,11 +10579,11 @@ "detectIndentation", "trimAutoWhitespace", "largeFileOptimizations", + "wordBasedSuggestions.off", + "wordBasedSuggestions.currentDocument", + "wordBasedSuggestions.matchingDocuments", + "wordBasedSuggestions.allDocuments", "wordBasedSuggestions", - "wordBasedSuggestionsMode.currentDocument", - "wordBasedSuggestionsMode.matchingDocuments", - "wordBasedSuggestionsMode.allDocuments", - "wordBasedSuggestionsMode", "semanticHighlighting.true", "semanticHighlighting.false", "semanticHighlighting.configuredByTheme", @@ -10102,6 +10605,7 @@ "renderSideBySideInlineBreakpoint", "useInlineViewWhenSpaceIsLimited", "renderMarginRevertIcon", + "renderGutterMenu", "ignoreTrimWhitespace", "renderIndicators", "codeLens", @@ -10121,12 +10625,6 @@ "dirtyFile", "dirtyFiles" ], - "vs/workbench/contrib/files/browser/workspaceWatcher": [ - "enospcError", - "learnMore", - "eshutdownError", - "reload" - ], "vs/workbench/contrib/files/browser/editors/textFileEditor": [ "textFileEditor", "openFolder", @@ -10142,83 +10640,7 @@ "cancel", "empty.msg", "conflict.1", - "conflict.N", - "edt.title.del", - "rename", - "create", - "edt.title.2", - "edt.title.1" - ], - "vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview": [ - "default" - ], - "vs/workbench/contrib/search/browser/searchActionsBase": [ - "search" - ], - "vs/workbench/contrib/search/browser/searchIcons": [ - "searchDetailsIcon", - "searchShowContextIcon", - "searchHideReplaceIcon", - "searchShowReplaceIcon", - "searchReplaceAllIcon", - "searchReplaceIcon", - "searchRemoveIcon", - "searchRefreshIcon", - "searchCollapseAllIcon", - "searchExpandAllIcon", - "searchShowAsTree", - "searchShowAsList", - "searchClearIcon", - "searchStopIcon", - "searchViewIcon", - "searchNewEditorIcon", - "searchOpenInFile" - ], - "vs/workbench/contrib/searchEditor/browser/searchEditor": [ - "moreSearch", - "searchScope.includes", - "label.includes", - "searchScope.excludes", - "label.excludes", - "runSearch", - "searchResultItem", - "searchEditor", - "textInputBoxBorder" - ], - "vs/workbench/contrib/searchEditor/browser/searchEditorInput": [ - "searchTitle.withQuery", - "searchTitle.withQuery", - "searchTitle" - ], - "vs/workbench/contrib/search/browser/patternInputWidget": [ - "defaultLabel", - "onlySearchInOpenEditors", - "useExcludesAndIgnoreFilesDescription" - ], - "vs/workbench/contrib/search/browser/searchMessage": [ - "unable to open trust", - "unable to open" - ], - "vs/workbench/browser/parts/views/viewPane": [ - "viewPaneContainerExpandedIcon", - "viewPaneContainerCollapsedIcon", - "viewToolbarAriaLabel" - ], - "vs/workbench/contrib/search/browser/searchResultsView": [ - "searchFolderMatch.other.label", - "searchFolderMatch.other.label", - "searchFileMatches", - "searchFileMatch", - "searchMatches", - "searchMatch", - "lineNumStr", - "numLinesStr", - "search", - "folderMatchAriaLabel", - "otherFilesAriaLabel", - "fileMatchAriaLabel", - "replacePreviewResultAria", - "searchResultAria" + "conflict.N" ], "vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess": [ "cannotRunGotoLine", @@ -10227,22 +10649,8 @@ "gotoLineLabelEmptyWithLimit", "gotoLineLabelEmpty" ], - "vs/workbench/contrib/search/browser/searchWidget": [ - "search.action.replaceAll.disabled.label", - "search.action.replaceAll.enabled.label", - "search.replace.toggle.button.title", - "label.Search", - "search.placeHolder", - "showContext", - "label.Replace", - "search.replace.placeHolder" - ], - "vs/workbench/services/search/common/queryBuilder": [ - "search.noWorkspaceWithName" - ], "vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess": [ "empty", - "gotoSymbol", { "key": "miGotoSymbolInEditor", "comment": [ @@ -10251,16 +10659,13 @@ }, "gotoSymbolQuickAccessPlaceholder", "gotoSymbolQuickAccess", - "gotoSymbolByCategoryQuickAccess" - ], - "vs/workbench/contrib/search/browser/symbolsQuickAccess": [ - "noSymbolResults", - "openToSide", - "openToBottom" + "gotoSymbolByCategoryQuickAccess", + "gotoSymbol" ], "vs/workbench/contrib/search/browser/anythingQuickAccess": [ "noAnythingResults", "recentlyOpenedSeparator", + "recentlyOpenedSeparator", "fileAndSymbolResultsSeparator", "fileResultsSeparator", "helpPickAriaLabel", @@ -10280,10 +10685,50 @@ "closeEditor", "filePickAriaLabelDirty" ], + "vs/workbench/contrib/search/browser/searchIcons": [ + "searchDetailsIcon", + "searchSeeMoreIcon", + "searchShowContextIcon", + "searchHideReplaceIcon", + "searchShowReplaceIcon", + "searchReplaceAllIcon", + "searchReplaceIcon", + "searchRemoveIcon", + "searchRefreshIcon", + "searchCollapseAllIcon", + "searchExpandAllIcon", + "searchShowAsTree", + "searchShowAsList", + "searchClearIcon", + "searchStopIcon", + "searchViewIcon", + "searchNewEditorIcon", + "searchOpenInFile", + "searchSparkleFilled", + "searchSparkleEmpty" + ], + "vs/workbench/contrib/search/browser/symbolsQuickAccess": [ + "noSymbolResults", + "openToSide", + "openToBottom" + ], + "vs/workbench/contrib/search/browser/searchWidget": [ + "search.action.replaceAll.disabled.label", + "search.action.replaceAll.enabled.label", + "search.replace.toggle.button.title", + "label.Search", + "search.placeHolder", + "showContext", + "label.Replace", + "search.replace.placeHolder" + ], "vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess": [ "QuickSearchSeeMoreFiles", "QuickSearchOpenInFile", - "QuickSearchMore" + "QuickSearchMore", + "showMore", + "enterSearchTerm", + "noAnythingResults" ], "vs/workbench/contrib/search/browser/searchActionsCopy": [ "copyMatchLabel", @@ -10291,10 +10736,7 @@ "copyAllLabel" ], "vs/workbench/contrib/search/browser/searchActionsFind": [ - "restrictResultsToFolder", - "excludeFolderFromSearch", - "revealInSideBar", - "findInFiles", + "search.expandRecursively", { "key": "miFindInFiles", "comment": [ @@ -10303,6 +10745,10 @@ }, "findInFiles.description", "findInFiles.args", + "restrictResultsToFolder", + "excludeFolderFromSearch", + "revealInSideBar", + "findInFiles", "findInFolder", "findInWorkspace" ], @@ -10332,14 +10778,14 @@ "file.replaceAll.label" ], "vs/workbench/contrib/search/browser/searchActionsSymbol": [ - "showTriggerActions", "showTriggerActions", { "key": "miGotoSymbolInWorkspace", "comment": [ "&& denotes a mnemonic" ] - } + }, + "showTriggerActions" ], "vs/workbench/contrib/search/browser/searchActionsTopBar": [ "clearSearchHistoryLabel", @@ -10354,114 +10800,162 @@ "vs/workbench/contrib/search/browser/searchActionsTextQuickAccess": [ "quickTextSearch" ], - "vs/workbench/contrib/debug/browser/debugColors": [ - "debugToolBarBackground", - "debugToolBarBorder", - "debugIcon.startForeground", - "debugIcon.pauseForeground", - "debugIcon.stopForeground", - "debugIcon.disconnectForeground", - "debugIcon.restartForeground", - "debugIcon.stepOverForeground", - "debugIcon.stepIntoForeground", - "debugIcon.stepOutForeground", - "debugIcon.continueForeground", - "debugIcon.stepBackForeground" + "vs/workbench/browser/parts/views/viewPane": [ + "viewPaneContainerExpandedIcon", + "viewPaneContainerCollapsedIcon", + "viewToolbarAriaLabel", + "viewAccessibilityHelp" ], - "vs/workbench/contrib/debug/browser/debugConsoleQuickAccess": [ - "workbench.action.debug.startDebug" + "vs/workbench/browser/labels": [ + "notebookCellLabel", + "notebookCellLabel" ], - "vs/workbench/contrib/debug/browser/callStackView": [ - { - "key": "running", - "comment": [ - "indicates state" - ] - }, - "showMoreStackFrames2", - { - "key": "session", - "comment": [ - "Session is a noun" - ] - }, - { - "key": "running", - "comment": [ - "indicates state" - ] - }, - "restartFrame", - "loadAllStackFrames", - "showMoreAndOrigin", - "showMoreStackFrames", - { - "key": "pausedOn", - "comment": [ - "indicates reason for program being paused" - ] - }, - "paused", - { - "comment": [ - "Debug is a noun in this context, not a verb." - ], - "key": "callStackAriaLabel" - }, - { - "key": "threadAriaLabel", - "comment": [ - "Placeholders stand for the thread name and the thread state.For example \"Thread 1\" and \"Stopped" - ] - }, - "stackFrameAriaLabel", + "vs/workbench/contrib/search/browser/searchActionsBase": [ + "search" + ], + "vs/workbench/contrib/search/browser/patternInputWidget": [ + "defaultLabel", + "onlySearchInOpenEditors", + "useExcludesAndIgnoreFilesDescription" + ], + "vs/workbench/contrib/search/browser/searchMessage": [ + "unable to open trust", + "unable to open" + ], + "vs/workbench/contrib/search/browser/searchResultsView": [ + "searchFolderMatch.other.label", + "searchFolderMatch.other.label", + "searchFileMatches", + "searchFileMatch", + "searchMatches", + "searchMatch", + "lineNumStr", + "numLinesStr", + "search", + "folderMatchAriaLabel", + "otherFilesAriaLabel", + "fileMatchAriaLabel", + "replacePreviewResultAria", + "searchResultAria" + ], + "vs/workbench/services/search/common/queryBuilder": [ + "search.noWorkspaceWithName" + ], + "vs/workbench/contrib/searchEditor/browser/searchEditor": [ + "moreSearch", + "searchScope.includes", + "label.includes", + "searchScope.excludes", + "label.excludes", + "runSearch", + "searchResultItem", + "searchEditor", + "textInputBoxBorder" + ], + "vs/workbench/contrib/scm/browser/activity": [ + "status.scm", + "scmPendingChangesBadge" + ], + "vs/workbench/contrib/scm/browser/dirtydiffDecorator": [ + "changes", + "change", + "multiChanges", + "multiChange", + "label.close", + "show previous change", + "show next change", { - "key": "running", + "key": "miGotoNextChange", "comment": [ - "indicates state" + "&& denotes a mnemonic" ] }, { - "key": "sessionLabel", + "key": "miGotoPreviousChange", "comment": [ - "Placeholders stand for the session name and the session state. For example \"Launch Program\" and \"Running\"" + "&& denotes a mnemonic" ] }, - "showMoreStackFrames", - "collapse" + "move to previous change", + "move to next change", + "editorGutterModifiedBackground", + "editorGutterAddedBackground", + "editorGutterDeletedBackground", + "minimapGutterModifiedBackground", + "minimapGutterAddedBackground", + "minimapGutterDeletedBackground", + "overviewRulerModifiedForeground", + "overviewRulerAddedForeground", + "overviewRulerDeletedForeground", + "diffAdded", + "diffModified", + "diffDeleted" ], - "vs/workbench/contrib/debug/browser/debugCommands": [ - "debug", - "restartDebug", - "stepOverDebug", - "stepIntoDebug", - "stepIntoTargetDebug", - "stepOutDebug", - "pauseDebug", - "disconnect", - "disconnectSuspend", - "stop", - "continueDebug", - "focusSession", - "selectAndStartDebugging", - "openLaunchJson", - "startDebug", - "startWithoutDebugging", - "nextDebugConsole", - "prevDebugConsole", - "openLoadedScript", - "callStackTop", - "callStackBottom", - "callStackUp", - "callStackDown", - "selectDebugConsole", - "selectDebugSession", - "chooseLocation", - "noExecutableCode", - "jumpToCursor", - "editor.debug.action.stepIntoTargets.none", - "addConfiguration", - "addInlineBreakpoint" + "vs/workbench/contrib/searchEditor/browser/searchEditorInput": [ + "searchEditorLabelIcon", + "searchTitle.withQuery", + "searchTitle.withQuery", + "searchTitle" + ], + "vs/workbench/contrib/scm/browser/scmViewPaneContainer": [ + "source control" + ], + "vs/workbench/contrib/scm/browser/scmRepositoriesViewPane": [ + "scm" + ], + "vs/workbench/contrib/workspace/common/workspace": [ + "workspaceTrustEnabledCtx", + "workspaceTrustedCtx" + ], + "vs/workbench/contrib/scm/browser/scmViewPane": [ + "scm.historyItemAdditionsForeground", + "scm.historyItemDeletionsForeground", + "scm.historyItemStatisticsBorder", + "scm.historyItemSelectedStatisticsBorder", + "fileChanged", + "filesChanged", + "insertion", + "insertions", + "deletion", + "deletions", + "scm", + "input", + "sortAction", + "scmChanges", + "repositories", + "incomingChanges", + "incomingChanges", + "always", + "auto", + "never", + "outgoingChanges", + "outgoingChanges", + "always", + "auto", + "never", + "showChangesSummary", + "setListViewMode", + "setTreeViewMode", + "repositorySortByDiscoveryTime", + "repositorySortByName", + "repositorySortByPath", + "sortChangesByName", + "sortChangesByPath", + "sortChangesByStatus", + "collapse all", + "expand all", + "scmInputMoreActions", + "scmInputCancelAction", + "label.close", + "syncSeparatorHeader", + "syncSeparatorHeaderAriaLabel", + "syncIncomingSeparatorHeader", + "syncIncomingSeparatorHeaderAriaLabel", + "syncOutgoingSeparatorHeader", + "syncOutgoingSeparatorHeaderAriaLabel", + "incomingChangesAriaLabel", + "outgoingChangesAriaLabel", + "allChanges" ], "vs/workbench/contrib/debug/browser/breakpointsView": [ "unverifiedExceptionBreakpoint", @@ -10472,12 +10966,17 @@ "read", "write", "access", + "expressionAndHitCount", "functionBreakpointPlaceholder", "functionBreakPointInputAriaLabel", "functionBreakpointExpressionPlaceholder", "functionBreakPointExpresionAriaLabel", "functionBreakpointHitCountPlaceholder", "functionBreakPointHitCountAriaLabel", + "dataBreakpointExpressionPlaceholder", + "dataBreakPointExpresionAriaLabel", + "dataBreakpointHitCountPlaceholder", + "dataBreakPointHitCountAriaLabel", "exceptionBreakpointAriaLabel", "exceptionBreakpointPlaceholder", "breakpoints", @@ -10499,103 +10998,138 @@ "logMessage", "expression", "hitCount", + "triggeredBy", "breakpoint", - "addFunctionBreakpoint", { "key": "miFunctionBreakpoint", "comment": [ "&& denotes a mnemonic" ] }, - "activateBreakpoints", + "dataBreakpointError", + "dataBreakpointAccessType", + "dataBreakpointMemoryRangePrompt", + "dataBreakpointMemoryRangePlaceholder", + "dataBreakpointAddrFormat", + "dataBreakpointAddrStartEnd", + { + "key": "miDataBreakpoint", + "comment": [ + "&& denotes a mnemonic" + ] + }, "removeBreakpoint", - "removeAllBreakpoints", { "key": "miRemoveAllBreakpoints", "comment": [ "&& denotes a mnemonic" ] }, - "enableAllBreakpoints", { "key": "miEnableAllBreakpoints", "comment": [ "&& denotes a mnemonic" ] }, - "disableAllBreakpoints", { "key": "miDisableAllBreakpoints", "comment": [ "&& denotes a mnemonic" ] }, - "reapplyAllBreakpoints", "editCondition", "editCondition", "editHitCount", "editBreakpoint", - "editHitCount" + "editHitCount", + "editMode", + "selectBreakpointMode", + "addFunctionBreakpoint", + "addDataBreakpointOnAddress", + "editDataBreakpointOnAddress", + "activateBreakpoints", + "removeAllBreakpoints", + "enableAllBreakpoints", + "disableAllBreakpoints", + "reapplyAllBreakpoints" ], - "vs/workbench/contrib/debug/browser/debugIcons": [ - "debugConsoleViewIcon", - "runViewIcon", - "variablesViewIcon", - "watchViewIcon", - "callStackViewIcon", - "breakpointsViewIcon", - "loadedScriptsViewIcon", - "debugBreakpoint", - "debugBreakpointDisabled", - "debugBreakpointUnverified", - "debugBreakpointFunction", - "debugBreakpointFunctionDisabled", - "debugBreakpointFunctionUnverified", - "debugBreakpointConditional", - "debugBreakpointConditionalDisabled", - "debugBreakpointConditionalUnverified", - "debugBreakpointData", - "debugBreakpointDataDisabled", - "debugBreakpointDataUnverified", - "debugBreakpointLog", - "debugBreakpointLogDisabled", - "debugBreakpointLogUnverified", - "debugBreakpointHint", - "debugBreakpointUnsupported", - "debugStackframe", - "debugStackframeFocused", - "debugGripper", - "debugRestartFrame", - "debugStop", - "debugDisconnect", - "debugRestart", - "debugStepOver", - "debugStepInto", - "debugStepOut", - "debugStepBack", - "debugPause", - "debugContinue", - "debugReverseContinue", - "debugRun", - "debugStart", - "debugConfigure", - "debugConsole", - "debugRemoveConfig", - "debugCollapseAll", - "callstackViewSession", - "debugConsoleClearAll", - "watchExpressionsRemoveAll", - "watchExpressionRemove", - "watchExpressionsAdd", - "watchExpressionsAddFuncBreakpoint", - "breakpointsRemoveAll", - "breakpointsActivate", - "debugConsoleEvaluationInput", - "debugConsoleEvaluationPrompt", - "debugInspectMemory" + "vs/workbench/contrib/debug/browser/callStackView": [ + { + "key": "running", + "comment": [ + "indicates state" + ] + }, + "showMoreStackFrames2", + { + "key": "session", + "comment": [ + "Session is a noun" + ] + }, + { + "key": "running", + "comment": [ + "indicates state" + ] + }, + "restartFrame", + "loadAllStackFrames", + "showMoreAndOrigin", + "showMoreStackFrames", + { + "key": "pausedOn", + "comment": [ + "indicates reason for program being paused" + ] + }, + "paused", + { + "comment": [ + "Debug is a noun in this context, not a verb." + ], + "key": "callStackAriaLabel" + }, + { + "key": "threadAriaLabel", + "comment": [ + "Placeholders stand for the thread name and the thread state.For example \"Thread 1\" and \"Stopped" + ] + }, + "stackFrameAriaLabel", + { + "key": "running", + "comment": [ + "indicates state" + ] + }, + { + "key": "sessionLabel", + "comment": [ + "Placeholders stand for the session name and the session state. For example \"Launch Program\" and \"Running\"" + ] + }, + "showMoreStackFrames", + "collapse" + ], + "vs/workbench/contrib/debug/browser/debugColors": [ + "debugToolBarBackground", + "debugToolBarBorder", + "debugIcon.startForeground", + "debugIcon.pauseForeground", + "debugIcon.stopForeground", + "debugIcon.disconnectForeground", + "debugIcon.restartForeground", + "debugIcon.stepOverForeground", + "debugIcon.stepIntoForeground", + "debugIcon.stepOutForeground", + "debugIcon.continueForeground", + "debugIcon.stepBackForeground" + ], + "vs/workbench/contrib/debug/browser/debugConsoleQuickAccess": [ + "workbench.action.debug.startDebug" ], "vs/workbench/contrib/debug/browser/debugEditorActions": [ - "toggleBreakpointAction", { "key": "miToggleBreakpoint", "comment": [ @@ -10616,6 +11150,13 @@ "&& denotes a mnemonic" ] }, + "triggerByBreakpointEditorAction", + { + "key": "miTriggerByBreakpoint", + "comment": [ + "&& denotes a mnemonic" + ] + }, "EditBreakpointEditorAction", { "key": "miEditBreakpoint", @@ -10623,23 +11164,18 @@ "&& denotes a mnemonic" ] }, - "openDisassemblyView", { "key": "miDisassemblyView", "comment": [ "&& denotes a mnemonic" ] }, - "toggleDisassemblyViewSourceCode", { "key": "mitogglesource", "comment": [ "&& denotes a mnemonic" ] }, - "runToCursor", - "evaluateInDebugConsole", - "addToWatch", "showDebugHover", "editor.debug.action.stepIntoTargets.notAvailable", { @@ -10650,7 +11186,93 @@ }, "goToNextBreakpoint", "goToPreviousBreakpoint", - "closeExceptionWidget" + "closeExceptionWidget", + "toggleBreakpointAction", + "openDisassemblyView", + "toggleDisassemblyViewSourceCode", + "toggleDisassemblyViewSourceCodeDescription", + "runToCursor", + "evaluateInDebugConsole", + "addToWatch" + ], + "vs/workbench/contrib/debug/browser/debugIcons": [ + "debugConsoleViewIcon", + "runViewIcon", + "variablesViewIcon", + "watchViewIcon", + "callStackViewIcon", + "breakpointsViewIcon", + "loadedScriptsViewIcon", + "debugBreakpoint", + "debugBreakpointDisabled", + "debugBreakpointUnverified", + "debugBreakpointPendingOnTrigger", + "debugBreakpointFunction", + "debugBreakpointFunctionDisabled", + "debugBreakpointFunctionUnverified", + "debugBreakpointConditional", + "debugBreakpointConditionalDisabled", + "debugBreakpointConditionalUnverified", + "debugBreakpointData", + "debugBreakpointDataDisabled", + "debugBreakpointDataUnverified", + "debugBreakpointLog", + "debugBreakpointLogDisabled", + "debugBreakpointLogUnverified", + "debugBreakpointHint", + "debugBreakpointUnsupported", + "debugStackframe", + "debugStackframeFocused", + "debugGripper", + "debugRestartFrame", + "debugStop", + "debugDisconnect", + "debugRestart", + "debugStepOver", + "debugStepInto", + "debugStepOut", + "debugStepBack", + "debugPause", + "debugContinue", + "debugReverseContinue", + "debugRun", + "debugStart", + "debugConfigure", + "debugConsole", + "debugRemoveConfig", + "debugCollapseAll", + "callstackViewSession", + "debugConsoleClearAll", + "watchExpressionsRemoveAll", + "watchExpressionRemove", + "watchExpressionsAdd", + "watchExpressionsAddFuncBreakpoint", + "watchExpressionsAddDataBreakpoint", + "breakpointsRemoveAll", + "breakpointsActivate", + "debugConsoleEvaluationInput", + "debugConsoleEvaluationPrompt", + "debugInspectMemory" + ], + "vs/workbench/contrib/debug/browser/debugQuickAccess": [ + "noDebugResults", + "customizeLaunchConfig", + { + "key": "contributed", + "comment": [ + "contributed is lower case because it looks better like that in UI. Nothing preceeds it. It is a name of the grouping of debug configurations." + ] + }, + "removeLaunchConfig", + { + "key": "providerAriaLabel", + "comment": [ + "Placeholder stands for the provider label. For example \"NodeJS\"." + ] + }, + "configure", + "addConfigTo", + "addConfiguration" ], "vs/workbench/contrib/debug/browser/debugService": [ "1activeSession", @@ -10691,58 +11313,61 @@ "breakpointAdded", "breakpointRemoved" ], - "vs/workbench/contrib/debug/browser/debugQuickAccess": [ - "noDebugResults", - "customizeLaunchConfig", - { - "key": "contributed", - "comment": [ - "contributed is lower case because it looks better like that in UI. Nothing preceeds it. It is a name of the grouping of debug configurations." - ] - }, - "removeLaunchConfig", - { - "key": "providerAriaLabel", - "comment": [ - "Placeholder stands for the provider label. For example \"NodeJS\"." - ] - }, - "configure", - "addConfigTo", + "vs/workbench/contrib/debug/browser/debugCommands": [ + "openLaunchJson", + "chooseLocation", + "noExecutableCode", + "jumpToCursor", + "editor.debug.action.stepIntoTargets.none", + "addInlineBreakpoint", + "debug", + "restartDebug", + "stepOverDebug", + "stepIntoDebug", + "stepIntoTargetDebug", + "stepOutDebug", + "pauseDebug", + "disconnect", + "disconnectSuspend", + "stop", + "continueDebug", + "focusSession", + "selectAndStartDebugging", + "startDebug", + "startWithoutDebugging", + "nextDebugConsole", + "prevDebugConsole", + "openLoadedScript", + "callStackTop", + "callStackBottom", + "callStackUp", + "callStackDown", + "copyAsExpression", + "copyValue", + "addToWatchExpressions", + "selectDebugConsole", + "selectDebugSession", "addConfiguration" ], - "vs/workbench/contrib/debug/browser/debugToolBar": [ - "notebook.moreRunActionsLabel", - "stepBackDebug", - "reverseContinue" - ], "vs/workbench/contrib/debug/browser/debugStatus": [ "status.debug", "debugTarget", "selectAndStartDebug" ], - "vs/workbench/contrib/debug/browser/disassemblyView": [ - "instructionNotAvailable", - "disassemblyTableColumnLabel", - "editorOpenedFromDisassemblyDescription", + "vs/workbench/contrib/debug/browser/debugToolBar": [ + "notebook.moreRunActionsLabel", + "stepBackDebug", + "reverseContinue" + ], + "vs/workbench/contrib/debug/browser/disassemblyView": [ + "instructionNotAvailable", + "disassemblyTableColumnLabel", + "editorOpenedFromDisassemblyDescription", "disassemblyView", "instructionAddress", "instructionBytes", "instructionText" ], - "vs/workbench/contrib/debug/browser/loadedScriptsView": [ - "loadedScriptsSession", - { - "comment": [ - "Debug is a noun in this context, not a verb." - ], - "key": "loadedScriptsAriaLabel" - }, - "loadedScriptsRootFolderAriaLabel", - "loadedScriptsSessionAriaLabel", - "loadedScriptsFolderAriaLabel", - "loadedScriptsSourceAriaLabel" - ], "vs/workbench/contrib/debug/browser/statusbarColorProvider": [ "statusBarDebuggingBackground", "statusBarDebuggingForeground", @@ -10751,6 +11376,9 @@ ], "vs/workbench/contrib/debug/browser/variablesView": [ "variableValueAriaLabel", + "removeVisualizer", + "variableValueAriaLabel", + "useVisualizer", "variablesAriaTreeLabel", "variableScopeAriaLabel", { @@ -10760,29 +11388,23 @@ ] }, "viewMemory.prompt", - "cancel", - "install", - "viewMemory.install.progress", "collapse" ], - "vs/workbench/contrib/debug/browser/watchExpressionsView": [ - "typeNewValue", - "watchExpressionInputAriaLabel", - "watchExpressionPlaceholder", + "vs/workbench/contrib/debug/browser/loadedScriptsView": [ + "loadedScriptsSession", { "comment": [ "Debug is a noun in this context, not a verb." ], - "key": "watchAriaTreeLabel" + "key": "loadedScriptsAriaLabel" }, - "watchExpressionAriaLabel", - "watchVariableAriaLabel", - "collapse", - "addWatchExpression", - "removeAllWatchExpressions" + "loadedScriptsRootFolderAriaLabel", + "loadedScriptsSessionAriaLabel", + "loadedScriptsFolderAriaLabel", + "loadedScriptsSourceAriaLabel", + "collapse" ], "vs/workbench/contrib/debug/browser/welcomeView": [ - "run", { "key": "openAFileWhichCanBeDebugged", "comment": [ @@ -10802,20 +11424,35 @@ { "key": "customizeRunAndDebugOpenFolder", "comment": [ - "Please do not translate the word \"commmand\", it is part of our internal syntax which must not change", + "Please do not translate the word \"command\", it is part of our internal syntax which must not change", + "Please do not translate \"launch.json\", it is the specific configuration file name", "{Locked=\"](command:{0})\"}" ] }, - "allDebuggersDisabled" + "allDebuggersDisabled", + "run" + ], + "vs/workbench/contrib/debug/browser/watchExpressionsView": [ + "typeNewValue", + "watchExpressionInputAriaLabel", + "watchExpressionPlaceholder", + { + "comment": [ + "Debug is a noun in this context, not a verb." + ], + "key": "watchAriaTreeLabel" + }, + "watchExpressionAriaLabel", + "watchVariableAriaLabel", + "collapse", + "addWatchExpression", + "removeAllWatchExpressions" ], "vs/workbench/contrib/debug/common/debugContentProvider": [ "unable", "canNotResolveSourceWithError", "canNotResolveSource" ], - "vs/workbench/contrib/debug/common/disassemblyViewInput": [ - "disassemblyInputName" - ], "vs/workbench/contrib/debug/common/debugLifecycle": [ "debug.debugSessionCloseConfirmationSingular", "debug.debugSessionCloseConfirmationPlural", @@ -10826,93 +11463,13 @@ ] } ], - "vs/workbench/contrib/debug/browser/breakpointWidget": [ - "breakpointWidgetLogMessagePlaceholder", - "breakpointWidgetHitCountPlaceholder", - "breakpointWidgetExpressionPlaceholder", - "expression", - "hitCount", - "logMessage", - "breakpointType" - ], - "vs/workbench/contrib/scm/browser/activity": [ - "status.scm", - "scmPendingChangesBadge" - ], - "vs/workbench/contrib/scm/browser/scmViewPaneContainer": [ - "source control" - ], - "vs/workbench/contrib/scm/browser/dirtydiffDecorator": [ - "changes", - "change", - "multiChanges", - "multiChange", - "label.close", - "show previous change", - "show next change", - { - "key": "miGotoNextChange", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "miGotoPreviousChange", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "move to previous change", - "move to next change", - "editorGutterModifiedBackground", - "editorGutterAddedBackground", - "editorGutterDeletedBackground", - "minimapGutterModifiedBackground", - "minimapGutterAddedBackground", - "minimapGutterDeletedBackground", - "overviewRulerModifiedForeground", - "overviewRulerAddedForeground", - "overviewRulerDeletedForeground" - ], - "vs/workbench/contrib/scm/browser/scmRepositoriesViewPane": [ - "scm" - ], - "vs/workbench/contrib/workspace/common/workspace": [ - "workspaceTrustEnabledCtx", - "workspaceTrustedCtx" - ], - "vs/workbench/contrib/scm/browser/scmViewPane": [ - "scm", - "input", - "sortAction", - "repositories", - "setListViewMode", - "setTreeViewMode", - "repositorySortByDiscoveryTime", - "repositorySortByName", - "repositorySortByPath", - "sortChangesByName", - "sortChangesByPath", - "sortChangesByStatus", - "collapse all", - "expand all", - "label.close", - "scm.providerBorder" - ], - "vs/workbench/contrib/scm/browser/scmSyncViewPane": [ - "scmSync", - "incoming", - "outgoing", - "refresh", - "setListViewMode", - "setTreeViewMode" + "vs/workbench/contrib/debug/common/disassemblyViewInput": [ + "disassemblyEditorLabelIcon", + "disassemblyInputName" ], - "vs/workbench/contrib/debug/browser/exceptionWidget": [ - "debugExceptionWidgetBorder", - "debugExceptionWidgetBackground", - "exceptionThrownWithId", - "exceptionThrown", - "close" + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariableCommands": [ + "copyWorkspaceVariableValue", + "executeNotebookVariableProvider" ], "vs/workbench/contrib/debug/browser/debugHover": [ { @@ -10929,6 +11486,13 @@ ] } ], + "vs/workbench/contrib/debug/browser/exceptionWidget": [ + "debugExceptionWidgetBorder", + "debugExceptionWidgetBackground", + "exceptionThrownWithId", + "exceptionThrown", + "close" + ], "vs/workbench/contrib/debug/common/debugModel": [ "invalidVariableAttributes", "startDebugFirst", @@ -10948,43 +11512,21 @@ }, "breakpointDirtydHover" ], - "vs/platform/history/browser/contextScopedHistoryWidget": [ - "suggestWidgetVisible" - ], - "vs/workbench/contrib/debug/browser/debugActionViewItems": [ - "debugLaunchConfigurations", - "noConfigurations", - "addConfigTo", - "addConfiguration", - "debugSession" - ], - "vs/platform/actions/browser/menuEntryActionViewItem": [ - "titleAndKb", - "titleAndKb", - "titleAndKbAndAlt" - ], - "vs/workbench/contrib/debug/browser/linkDetector": [ - "followForwardedLink", - "followLink", - "fileLinkWithPathMac", - "fileLinkWithPath", - "fileLinkMac", - "fileLink" - ], - "vs/workbench/contrib/debug/common/replModel": [ - "consoleCleared" - ], - "vs/workbench/contrib/debug/browser/replViewer": [ - "debugConsole", - "replVariableAriaLabel", - { - "key": "occurred", - "comment": [ - "Front will the value of the debug console element. Placeholder will be replaced by a number which represents occurrance count." - ] - }, - "replRawObjectAriaLabel", - "replGroup" + "vs/workbench/contrib/debug/browser/breakpointWidget": [ + "breakpointWidgetLogMessagePlaceholder", + "breakpointWidgetHitCountPlaceholder", + "breakpointWidgetExpressionPlaceholder", + "expression", + "hitCount", + "logMessage", + "triggeredBy", + "breakpointType", + "bpMode", + "noTriggerByBreakpoint", + "triggerByLoading", + "noBpSource", + "selectBreakpoint", + "ok" ], "vs/workbench/contrib/markers/browser/markersView": [ "showing filtered problems", @@ -10994,7 +11536,6 @@ ], "vs/workbench/contrib/markers/browser/messages": [ "problems.view.toggle.label", - "problems.view.focus.label", "problems.panel.configuration.title", "problems.panel.configuration.autoreveal", "problems.panel.configuration.viewMode", @@ -11002,7 +11543,6 @@ "problems.panel.configuration.compareOrder", "problems.panel.configuration.compareOrder.severity", "problems.panel.configuration.compareOrder.position", - "markers.panel.title.problems", "markers.panel.no.problems.build", "markers.panel.no.problems.activeFile.build", "markers.panel.no.problems.filters", @@ -11039,44 +11579,9 @@ "problems.tree.aria.label.marker", "problems.tree.aria.label.marker.nosource", "problems.tree.aria.label.relatedinfo.message", - "errors.warnings.show.label" - ], - "vs/workbench/browser/parts/views/viewFilter": [ - "more filters" - ], - "vs/workbench/contrib/mergeEditor/browser/commands/commands": [ - "title", - "layout.mixed", - "layout.column", - "showNonConflictingChanges", - "layout.showBase", - "layout.showBaseTop", - "layout.showBaseCenter", - "mergeEditor", - "openfile", - "merge.goToNextUnhandledConflict", - "merge.goToPreviousUnhandledConflict", - "merge.toggleCurrentConflictFromLeft", - "merge.toggleCurrentConflictFromRight", - "mergeEditor.compareInput1WithBase", - "mergeEditor.compareWithBase", - "mergeEditor.compareInput2WithBase", - "mergeEditor.compareWithBase", - "merge.openBaseEditor", - "merge.acceptAllInput1", - "merge.acceptAllInput2", - "mergeEditor.resetResultToBaseAndAutoMerge", - "mergeEditor.resetResultToBaseAndAutoMerge.short", - "mergeEditor.resetChoice", - "mergeEditor.acceptMerge", - "mergeEditor.acceptMerge.unhandledConflicts.message", - "mergeEditor.acceptMerge.unhandledConflicts.detail", - { - "key": "mergeEditor.acceptMerge.unhandledConflicts.accept", - "comment": [ - "&& denotes a mnemonic" - ] - } + "errors.warnings.show.label", + "problems.view.focus.label", + "markers.panel.title.problems" ], "vs/workbench/contrib/markers/browser/markersFileDecorations": [ "label", @@ -11084,46 +11589,196 @@ "tooltip.N", "markers.showOnFile" ], - "vs/workbench/contrib/mergeEditor/browser/mergeEditorInput": [ - "name" - ], - "vs/workbench/contrib/mergeEditor/browser/commands/devCommands": [ - "mergeEditor", - "merge.dev.copyState", - "mergeEditor.name", - "mergeEditor.noActiveMergeEditor", - "mergeEditor.name", - "mergeEditor.successfullyCopiedMergeEditorContents", - "merge.dev.saveContentsToFolder", - "mergeEditor.name", - "mergeEditor.noActiveMergeEditor", - "mergeEditor.selectFolderToSaveTo", - "mergeEditor.name", - "mergeEditor.successfullySavedMergeEditorContentsToFolder", - "merge.dev.loadContentsFromFolder", - "mergeEditor.selectFolderToSaveTo" + "vs/workbench/browser/parts/views/viewFilter": [ + "more filters" ], - "vs/workbench/contrib/mergeEditor/browser/view/mergeEditor": [ - "mergeEditor" + "vs/platform/actions/browser/menuEntryActionViewItem": [ + "titleAndKb", + "titleAndKb", + "titleAndKbAndAlt" ], - "vs/workbench/contrib/comments/common/commentContextKeys": [ - "hasCommentingRange", - "editorHasCommentingRange", - "hasCommentingProvider", - "commentThreadIsEmpty", - "commentIsEmpty", - "comment", - "commentThread", - "commentController", - "commentFocused" + "vs/workbench/contrib/debug/browser/debugActionViewItems": [ + "debugLaunchConfigurations", + "noConfigurations", + "addConfigTo", + "addConfiguration", + "debugSession" ], - "vs/workbench/contrib/url/browser/trustedDomains": [ - "trustedDomain.manageTrustedDomain", - "trustedDomain.trustDomain", - "trustedDomain.trustAllPorts", - "trustedDomain.trustSubDomain", - "trustedDomain.trustAllDomains", - "trustedDomain.manageTrustedDomains" + "vs/workbench/contrib/comments/browser/commentsTreeViewer": [ + "commentsCountReplies", + "commentsCountReply", + "commentCount", + "imageWithLabel", + "image", + "outdated", + "commentLine", + "commentRange", + "lastReplyFrom", + "comments.view.title" + ], + "vs/workbench/contrib/comments/browser/commentsView": [ + "comments.filter.placeholder", + "comments.filter.ariaLabel", + "showing filtered results", + "acessibleViewHint", + "acessibleViewHintNoKbOpen", + "resourceWithCommentLabelOutdated", + "resourceWithCommentLabel", + "resourceWithCommentLabelFileOutdated", + "resourceWithCommentLabelFile", + "resourceWithRepliesLabel", + "replyCount", + "rootCommentsLabel", + "resourceWithCommentThreadsLabel" + ], + "vs/workbench/contrib/comments/browser/commentsController": [ + "commentRange", + "commentRangeStart", + "hasCommentRangesKb", + "hasCommentRangesNoKb", + "hasCommentRanges", + "pickCommentService" + ], + "vs/workbench/contrib/accessibility/browser/accessibilityConfiguration": [ + "accessibility.announcement.deprecationMessage", + "accessibilityConfigurationTitle", + "sound.enabled.auto", + "sound.enabled.on", + "sound.enabled.off", + "announcement.enabled.auto", + "announcement.enabled.off", + "verbosity.terminal.description", + "verbosity.diffEditor.description", + "verbosity.chat.description", + "verbosity.interactiveEditor.description", + "verbosity.inlineCompletions.description", + "verbosity.keybindingsEditor.description", + "verbosity.notebook", + "verbosity.hover", + "verbosity.notification", + "verbosity.emptyEditorHint", + "verbosity.comments", + "verbosity.diffEditorActive", + "announcement.save", + "announcement.save.userGesture", + "announcement.save.always", + "announcement.save.never", + "announcement.clear", + "announcement.format", + "announcement.format.userGesture", + "announcement.format.always", + "announcement.format.never", + "announcement.breakpoint", + "announcement.error", + "announcement.warning", + "announcement.foldedArea", + "announcement.terminalQuickFix", + "announcement.terminalBell", + "announcement.terminalCommandFailed", + "announcement.taskFailed", + "announcement.taskCompleted", + "announcement.chatRequestSent", + "announcement.progress", + "announcement.noInlayHints", + "announcement.lineHasBreakpoint", + "announcement.notebookCellCompleted", + "announcement.notebookCellFailed", + "announcement.onDebugBreak", + "terminal.integrated.accessibleView.closeOnKeyPress", + "accessibility.signals.sounds.volume", + "accessibility.signals.debouncePositionChanges", + "accessibility.signals.lineHasBreakpoint", + "accessibility.signals.lineHasBreakpoint.sound", + "accessibility.signals.lineHasBreakpoint.announcement", + "accessibility.signals.lineHasInlineSuggestion", + "accessibility.signals.lineHasInlineSuggestion.sound", + "accessibility.signals.lineHasError", + "accessibility.signals.lineHasError.sound", + "accessibility.signals.lineHasError.announcement", + "accessibility.signals.lineHasFoldedArea", + "accessibility.signals.lineHasFoldedArea.sound", + "accessibility.signals.lineHasFoldedArea.announcement", + "accessibility.signals.lineHasWarning", + "accessibility.signals.lineHasWarning.sound", + "accessibility.signals.lineHasWarning.announcement", + "accessibility.signals.positionHasError", + "accessibility.signals.positionHasError.sound", + "accessibility.signals.positionHasError.announcement", + "accessibility.signals.positionHasWarning", + "accessibility.signals.positionHasWarning.sound", + "accessibility.signals.positionHasWarning.announcement", + "accessibility.signals.onDebugBreak", + "accessibility.signals.onDebugBreak.sound", + "accessibility.signals.onDebugBreak.announcement", + "accessibility.signals.noInlayHints", + "accessibility.signals.noInlayHints.sound", + "accessibility.signals.noInlayHints.announcement", + "accessibility.signals.taskCompleted", + "accessibility.signals.taskCompleted.sound", + "accessibility.signals.taskCompleted.announcement", + "accessibility.signals.taskFailed", + "accessibility.signals.taskFailed.sound", + "accessibility.signals.taskFailed.announcement", + "accessibility.signals.terminalCommandFailed", + "accessibility.signals.terminalCommandFailed.sound", + "accessibility.signals.terminalCommandFailed.announcement", + "accessibility.signals.terminalQuickFix", + "accessibility.signals.terminalQuickFix.sound", + "accessibility.signals.terminalQuickFix.announcement", + "accessibility.signals.terminalBell", + "accessibility.signals.terminalBell.sound", + "accessibility.signals.terminalBell.announcement", + "accessibility.signals.diffLineInserted", + "accessibility.signals.sound", + "accessibility.signals.diffLineModified", + "accessibility.signals.diffLineModified.sound", + "accessibility.signals.diffLineDeleted", + "accessibility.signals.diffLineDeleted.sound", + "accessibility.signals.notebookCellCompleted", + "accessibility.signals.notebookCellCompleted.sound", + "accessibility.signals.notebookCellCompleted.announcement", + "accessibility.signals.notebookCellFailed", + "accessibility.signals.notebookCellFailed.sound", + "accessibility.signals.notebookCellFailed.announcement", + "accessibility.signals.chatRequestSent", + "accessibility.signals.chatRequestSent.sound", + "accessibility.signals.chatRequestSent.announcement", + "accessibility.signals.progress", + "accessibility.signals.progress.sound", + "accessibility.signals.progress.announcement", + "accessibility.signals.chatResponseReceived", + "accessibility.signals.chatResponseReceived.sound", + "accessibility.signals.voiceRecordingStarted", + "accessibility.signals.voiceRecordingStarted.sound", + "accessibility.signals.voiceRecordingStopped", + "accessibility.signals.voiceRecordingStopped.sound", + "accessibility.signals.clear", + "accessibility.signals.clear.sound", + "accessibility.signals.clear.announcement", + "accessibility.signals.save", + "accessibility.signals.save.sound", + "accessibility.signals.save.sound.userGesture", + "accessibility.signals.save.sound.always", + "accessibility.signals.save.sound.never", + "accessibility.signals.save.announcement", + "accessibility.signals.save.announcement.userGesture", + "accessibility.signals.save.announcement.always", + "accessibility.signals.save.announcement.never", + "accessibility.signals.format", + "accessibility.signals.format.sound", + "accessibility.signals.format.userGesture", + "accessibility.signals.format.always", + "accessibility.signals.format.never", + "accessibility.signals.format.announcement", + "accessibility.signals.format.announcement.userGesture", + "accessibility.signals.format.announcement.always", + "accessibility.signals.format.announcement.never", + "dimUnfocusedEnabled", + "dimUnfocusedOpacity", + "accessibility.hideAccessibleView", + "voice.speechTimeout", + "voice.speechLanguage", + "speechLanguage.auto" ], "vs/workbench/contrib/comments/browser/commentsEditorContribution": [ "comments.nextCommentingRange", @@ -11135,6 +11790,70 @@ "comments.expandAll", "comments.expandUnresolved" ], + "vs/workbench/contrib/mergeEditor/browser/commands/devCommands": [ + "mergeEditor.name", + "mergeEditor.noActiveMergeEditor", + "mergeEditor.name", + "mergeEditor.successfullyCopiedMergeEditorContents", + "mergeEditor.name", + "mergeEditor.noActiveMergeEditor", + "mergeEditor.selectFolderToSaveTo", + "mergeEditor.name", + "mergeEditor.successfullySavedMergeEditorContentsToFolder", + "mergeEditor.selectFolderToSaveTo", + "mergeEditor", + "merge.dev.copyState", + "merge.dev.saveContentsToFolder", + "merge.dev.loadContentsFromFolder" + ], + "vs/workbench/contrib/mergeEditor/browser/mergeEditorInput": [ + "name" + ], + "vs/workbench/contrib/mergeEditor/browser/commands/commands": [ + "mergeEditor.compareWithBase", + "mergeEditor.compareWithBase", + "mergeEditor.resetResultToBaseAndAutoMerge.short", + "mergeEditor.acceptMerge.unhandledConflicts.message", + "mergeEditor.acceptMerge.unhandledConflicts.detail", + { + "key": "mergeEditor.acceptMerge.unhandledConflicts.accept", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "title", + "layout.mixed", + "layout.column", + "showNonConflictingChanges", + "layout.showBase", + "layout.showBaseTop", + "layout.showBaseCenter", + "mergeEditor", + "openfile", + "merge.goToNextUnhandledConflict", + "merge.goToPreviousUnhandledConflict", + "merge.toggleCurrentConflictFromLeft", + "merge.toggleCurrentConflictFromRight", + "mergeEditor.compareInput1WithBase", + "mergeEditor.compareInput2WithBase", + "merge.openBaseEditor", + "merge.acceptAllInput1", + "merge.acceptAllInput2", + "mergeEditor.resetResultToBaseAndAutoMerge", + "mergeEditor.resetChoice", + "mergeEditor.acceptMerge" + ], + "vs/workbench/contrib/mergeEditor/browser/view/mergeEditor": [ + "mergeEditor" + ], + "vs/workbench/contrib/url/browser/trustedDomains": [ + "trustedDomain.trustDomain", + "trustedDomain.trustAllPorts", + "trustedDomain.trustSubDomain", + "trustedDomain.trustAllDomains", + "trustedDomain.manageTrustedDomains", + "trustedDomain.manageTrustedDomain" + ], "vs/workbench/contrib/url/browser/trustedDomainsValidator": [ "openExternalLinkAt", { @@ -11166,11 +11885,13 @@ "vs/workbench/contrib/webviewPanel/browser/webviewEditor": [ "context.activeWebviewId" ], - "vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService": [ - "selectOpenerDefaultLabel.web", - "selectOpenerDefaultLabel", - "selectOpenerConfigureTitle", - "selectOpenerPlaceHolder" + "vs/workbench/contrib/customEditor/common/customEditor": [ + "context.customEditor" + ], + "vs/workbench/contrib/customEditor/browser/customEditorInput": [ + "editorUnsupportedInWindow", + "reopenInOriginalWindow", + "editorCannotMove" ], "vs/workbench/contrib/externalUriOpener/common/configuration": [ "externalUriOpeners", @@ -11178,148 +11899,61 @@ "externalUriOpeners.uri", "externalUriOpeners.defaultId" ], - "vs/workbench/contrib/customEditor/common/customEditor": [ - "context.customEditor" + "vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService": [ + "selectOpenerDefaultLabel.web", + "selectOpenerDefaultLabel", + "selectOpenerConfigureTitle", + "selectOpenerPlaceHolder" ], "vs/workbench/contrib/extensions/common/extensionsInput": [ + "extensionsEditorLabelIcon", "extensionsInputName" ], - "vs/workbench/contrib/extensions/common/extensionsUtils": [ - "disableOtherKeymapsConfirmation", - "yes", - "no" - ], - "vs/workbench/contrib/extensions/browser/extensionEditor": [ - "extension version", - "preRelease", - "name", - "preview", - "preview", - "builtin", - "publisher", - "install count", - "rating", - "details", - "detailstooltip", - "contributions", - "contributionstooltip", - "changelog", - "changelogtooltip", - "dependencies", - "dependenciestooltip", - "extensionpack", - "extensionpacktooltip", - "runtimeStatus", - "runtimeStatus description", - "noReadme", - "Readme title", - "extension pack", - "noReadme", - "Readme title", - "categories", - "Marketplace", - "repository", - "license", - "resources", - "Marketplace Info", - "published", - "last released", - "last updated", - "id", - "noChangelog", - "Changelog title", - "noContributions", - "noContributions", - "noDependencies", - "activation reason", - "startup", - "activation time", - "activatedBy", - "not yet activated", - "uncaught errors", - "messages", - "noStatus", - "settings", - "setting name", - "description", - "default", - "debuggers", - "debugger name", - "debugger type", - "viewContainers", - "view container id", - "view container title", - "view container location", - "views", - "view id", - "view name", - "view location", - "localizations", - "localizations language id", - "localizations language name", - "localizations localized language name", - "customEditors", - "customEditors view type", - "customEditors priority", - "customEditors filenamePattern", - "codeActions", - "codeActions.title", - "codeActions.kind", - "codeActions.description", - "codeActions.languages", - "authentication", - "authentication.label", - "authentication.id", - "colorThemes", - "iconThemes", - "productThemes", - "colors", - "colorId", - "description", - "defaultDark", - "defaultLight", - "defaultHC", - "JSON Validation", - "fileMatch", - "schema", - "commands", - "command name", - "command title", - "keyboard shortcuts", - "menuContexts", - "languages", - "language id", - "language name", - "file extensions", - "grammar", - "snippets", - "activation events", - "Notebooks", - "Notebook id", - "Notebook name", - "NotebookRenderers", - "Notebook renderer name", - "Notebook mimetypes", - "find", - "find next", - "find previous" - ], - "vs/workbench/contrib/extensions/common/extensionsFileTemplate": [ - "app.extensions.json.title", - "app.extensions.json.recommendations", - "app.extension.identifier.errorMessage", - "app.extensions.json.unwantedRecommendations", - "app.extension.identifier.errorMessage" + "vs/workbench/contrib/extensions/browser/extensionsIcons": [ + "extensionsViewIcon", + "manageExtensionIcon", + "clearSearchResultsIcon", + "refreshIcon", + "filterIcon", + "installLocalInRemoteIcon", + "installWorkspaceRecommendedIcon", + "configureRecommendedIcon", + "syncEnabledIcon", + "syncIgnoredIcon", + "remoteIcon", + "installCountIcon", + "ratingIcon", + "verifiedPublisher", + "preReleaseIcon", + "sponsorIcon", + "starFullIcon", + "starHalfIcon", + "starEmptyIcon", + "errorIcon", + "warningIcon", + "infoIcon", + "trustIcon", + "activationtimeIcon" ], - "vs/workbench/contrib/extensions/browser/extensionsActivationProgress": [ - "activation" - ], - "vs/workbench/contrib/extensions/browser/extensionsDependencyChecker": [ + "vs/workbench/contrib/extensions/browser/extensionsViews": [ "extensions", - "auto install missing deps", - "finished installing missing deps", - "reload", - "no missing deps" + "offline error", + "error", + "no extensions found", + "suggestProxyError", + "open user settings", + "no local extensions", + "extension.arialabel.verifiedPublisher", + "extension.arialabel.publisher", + "extension.arialabel.deprecated", + "extension.arialabel.rating" + ], + "vs/platform/dnd/browser/dnd": [ + "fileTooLarge" + ], + "vs/platform/actions/browser/toolbar": [ + "hide", + "resetThisMenu" ], "vs/workbench/contrib/extensions/browser/extensionsActions": [ "VS Code for Web", @@ -11331,6 +11965,8 @@ ] }, "close", + "install prerelease", + "cancel", "signature verification failed", "install anyway", "cancel", @@ -11360,6 +11996,7 @@ "install confirmation", "installExtensionStart", "installExtensionComplete", + "install workspace version", "install pre-release", "install pre-release version", "install", @@ -11386,17 +12023,22 @@ "update", "updateExtensionStart", "updateExtensionComplete", - "ignoreUpdates", - "ignoreExtensionUpdate", + "enableAutoUpdate", + "disableAutoUpdate", + "toggleAutoUpdatesForPublisherLabel", + "ignoreExtensionUpdatePublisher", + "enableAutoUpdate", + "disableAutoUpdate", "migrateExtension", "migrate to", "migrate", "manage", "manage", - "switch to pre-release version", - "switch to pre-release version tooltip", - "switch to release version", - "switch to release version tooltip", + "togglePreRleaseLabel", + "togglePreRleaseDisableLabel", + "togglePreRleaseDisableTooltip", + "switchToPreReleaseLabel", + "switchToPreReleaseTooltip", "install another version", "no versions", "pre-release", @@ -11412,17 +12054,14 @@ "disableGloballyActionToolTip", "enableAction", "disableAction", - "reloadAction", - "reload required", + "reload window", + "restart extensions", + "restart product", + "update product", "current", - "workbench.extensions.action.setColorTheme", "select color theme", - "workbench.extensions.action.setFileIconTheme", "select file icon theme", - "workbench.extensions.action.setProductIconTheme", "select product icon theme", - "workbench.extensions.action.setDisplayLanguage", - "workbench.extensions.action.clearLanguage", "showRecommendedExtension", "installRecommendedExtension", "ignoreExtensionRecommendation", @@ -11464,6 +12103,7 @@ "learn more", "Cannot be enabled", "learn more", + "manage access", "Install language pack also in remote server", "Install language pack also locally", "enabled remotely", @@ -11499,7 +12139,79 @@ "extensionButtonSeparator", "extensionButtonProminentBackground", "extensionButtonProminentForeground", - "extensionButtonProminentHoverBackground" + "extensionButtonProminentHoverBackground", + "enableAutoUpdateLabel", + "workbench.extensions.action.setColorTheme", + "workbench.extensions.action.setFileIconTheme", + "workbench.extensions.action.setProductIconTheme", + "workbench.extensions.action.setDisplayLanguage", + "workbench.extensions.action.clearLanguage" + ], + "vs/workbench/contrib/extensions/browser/extensionEditor": [ + "extension version", + "name", + "preview", + "preview", + "builtin", + "publisher", + "install count", + "rating", + "workspace extension", + "local extension", + "details", + "detailstooltip", + "features", + "featurestooltip", + "changelog", + "changelogtooltip", + "dependencies", + "dependenciestooltip", + "extensionpack", + "extensionpacktooltip", + "noReadme", + "Readme title", + "extension pack", + "noReadme", + "Readme title", + "categories", + "Marketplace", + "issues", + "repository", + "license", + "resources", + "Marketplace Info", + "published", + "last released", + "last updated", + "id", + "noChangelog", + "Changelog title", + "noDependencies", + "find", + "find next", + "find previous" + ], + "vs/workbench/contrib/extensions/common/extensionsFileTemplate": [ + "app.extensions.json.title", + "app.extensions.json.recommendations", + "app.extension.identifier.errorMessage", + "app.extensions.json.unwantedRecommendations", + "app.extension.identifier.errorMessage" + ], + "vs/workbench/contrib/extensions/browser/extensionsActivationProgress": [ + "activation" + ], + "vs/workbench/contrib/extensions/browser/extensionsDependencyChecker": [ + "extensions", + "auto install missing deps", + "finished installing missing deps", + "reload", + "no missing deps" + ], + "vs/workbench/contrib/extensions/common/extensionsUtils": [ + "disableOtherKeymapsConfirmation", + "yes", + "no" ], "vs/workbench/contrib/extensions/browser/extensionsQuickAccess": [ "type", @@ -11508,7 +12220,6 @@ "manage" ], "vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService": [ - "neverShowAgain", "ignoreExtensionRecommendations", "ignoreAll", "no", @@ -11529,61 +12240,13 @@ "Placeholder string is the name of the software that is installed." ] }, + "donotShowAgain", + "donotShowAgainExtension", + "donotShowAgainExtensionSingle", "install", "install and do no sync", "show recommendations" ], - "vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant": [ - "restartExtensionHost.reason" - ], - "vs/workbench/contrib/extensions/browser/extensionsWorkbenchService": [ - "Manifest is not found", - "postUninstallTooltip", - "postUpdateTooltip", - "enable locally", - "enable remote", - "postEnableTooltip", - "postEnableTooltip", - "postDisableTooltip", - "postEnableTooltip", - "postEnableTooltip", - "malicious", - "incompatible", - "uninstallingExtension", - "not found", - "installing extension", - "installing named extension", - "disable all", - "singleDependentError", - "twoDependentsError", - "multipleDependentsError" - ], - "vs/workbench/contrib/extensions/browser/extensionsIcons": [ - "extensionsViewIcon", - "manageExtensionIcon", - "clearSearchResultsIcon", - "refreshIcon", - "filterIcon", - "installLocalInRemoteIcon", - "installWorkspaceRecommendedIcon", - "configureRecommendedIcon", - "syncEnabledIcon", - "syncIgnoredIcon", - "remoteIcon", - "installCountIcon", - "ratingIcon", - "verifiedPublisher", - "preReleaseIcon", - "sponsorIcon", - "starFullIcon", - "starHalfIcon", - "starEmptyIcon", - "errorIcon", - "warningIcon", - "infoIcon", - "trustIcon", - "activationtimeIcon" - ], "vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor": [ { "key": "starActivation", @@ -11629,12 +12292,18 @@ "extensionActivating", "unresponsive.title", "errors", + "requests count", + "session requests count", + "requests count title", "runtimeExtensions", "copy id", "disable workspace", "disable", "showRuntimeExtensions" ], + "vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant": [ + "restartExtensionHost.reason" + ], "vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider": [ "exampleExtension" ], @@ -11643,39 +12312,78 @@ "showDeprecated", "neverShowAgain" ], - "vs/workbench/contrib/extensions/browser/extensionsViews": [ - "extensions", - "offline error", - "error", - "no extensions found", - "suggestProxyError", - "open user settings", - "no local extensions", - "extension.arialabel.verifiedPublisher", - "extension.arialabel.publisher", - "extension.arialabel.deprecated", - "extension.arialabel.rating" - ], - "vs/platform/dnd/browser/dnd": [ - "fileTooLarge" + "vs/workbench/contrib/extensions/browser/extensionsWorkbenchService": [ + "Manifest is not found", + "restart", + "reload", + "restart extensions", + "postUninstallTooltip", + "postUpdateDownloadTooltip", + "postUpdateUpdateTooltip", + "postUpdateRestartTooltip", + "postUpdateTooltip", + "enable locally", + "enable remote", + "postEnableTooltip", + "postEnableTooltip", + "postDisableTooltip", + "postEnableTooltip", + "postEnableTooltip", + "malicious", + "not found version", + "not found", + { + "key": "installButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "installButtonLabelWithAction", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "open", + "installExtensionTitle", + "installExtensionMessage", + "installVSIXMessage", + "sync extension", + "unknown", + "enableExtensionTitle", + "enableExtensionMessage", + { + "key": "enableButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "enableButtonLabelWithAction", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "incompatible", + "uninstallingExtension", + "installing named extension", + "installing extension", + "disable all", + "singleDependentError", + "twoDependentsError", + "multipleDependentsError" ], "vs/workbench/contrib/terminal/browser/terminal.contribution": [ "tasksQuickAccessPlaceholder", "tasksQuickAccessHelp", - "terminal", - "terminal", { "key": "miToggleIntegratedTerminal", "comment": [ "&& denotes a mnemonic" ] - } - ], - "vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution": [ - "workbench.action.terminal.showTextureAtlas", - "workbench.action.terminal.writeDataToTerminal", - "workbench.action.terminal.writeDataToTerminal.prompt", - "workbench.action.terminal.restartPtyHost" + }, + "terminal", + "terminal" ], "vs/workbench/contrib/terminal/browser/terminalView": [ "terminal.useMonospace", @@ -11683,22 +12391,40 @@ "terminals", "terminalConnectingLabel" ], - "vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution": [ - "workbench.action.terminal.focusAccessibleBuffer", - "workbench.action.terminal.accessibleBufferGoToNextCommand", - "workbench.action.terminal.accessibleBufferGoToPreviousCommand" + "vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution": [ + "workbench.action.terminal.writeDataToTerminal.prompt", + "terminalDevMode", + "workbench.action.terminal.showTextureAtlas", + "workbench.action.terminal.writeDataToTerminal", + "workbench.action.terminal.restartPtyHost" ], "vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution": [ - "workbench.action.terminal.showEnvironmentContributions", "envChanges", "extension", - "ScopedEnvironmentContributionInfo" + "ScopedEnvironmentContributionInfo", + "workbench.action.terminal.showEnvironmentContributions" + ], + "vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution": [ + "workbench.action.terminal.focusAccessibleBuffer", + "workbench.action.terminal.accessibleBufferGoToNextCommand", + "workbench.action.terminal.accessibleBufferGoToPreviousCommand", + "workbench.action.terminal.scrollToBottomAccessibleView", + "workbench.action.terminal.scrollToTopAccessibleView" ], "vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution": [ "workbench.action.terminal.openDetectedLink", "workbench.action.terminal.openLastUrlLink", + "workbench.action.terminal.openLastUrlLink.description", "workbench.action.terminal.openLastLocalFileLink" ], + "vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution": [ + "workbench.action.terminal.showQuickFixes" + ], + "vs/workbench/contrib/terminalContrib/zoom/browser/terminal.zoom.contribution": [ + "fontZoomIn", + "fontZoomOut", + "fontZoomReset" + ], "vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution": [ "workbench.action.terminal.focusFind", "workbench.action.terminal.hideFind", @@ -11709,8 +12435,13 @@ "workbench.action.terminal.findPrevious", "workbench.action.terminal.searchWorkspace" ], - "vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution": [ - "workbench.action.terminal.showQuickFixes" + "vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution": [ + "workbench.action.terminal.selectPrevSuggestion", + "workbench.action.terminal.selectPrevPageSuggestion", + "workbench.action.terminal.selectNextSuggestion", + "workbench.action.terminal.selectNextPageSuggestion", + "workbench.action.terminal.acceptSelectedSuggestion", + "workbench.action.terminal.hideSuggestWidget" ], "vs/workbench/contrib/tasks/browser/runAutomaticTasks": [ "workbench.action.tasks.manageAutomaticRunning", @@ -11787,9 +12518,19 @@ "eslint-stylish", "go" ], - "vs/workbench/contrib/tasks/common/jsonSchema_v2": [ - "JsonSchema.shell", - "JsonSchema.tasks.isShellCommand.deprecated", + "vs/workbench/contrib/tasks/common/jsonSchema_v1": [ + "JsonSchema.version.deprecated", + "JsonSchema.version", + "JsonSchema._runner", + "JsonSchema.runner", + "JsonSchema.windows", + "JsonSchema.mac", + "JsonSchema.linux", + "JsonSchema.shell" + ], + "vs/workbench/contrib/tasks/common/jsonSchema_v2": [ + "JsonSchema.shell", + "JsonSchema.tasks.isShellCommand.deprecated", "JsonSchema.hide", "JsonSchema.tasks.dependsOn.identifier", "JsonSchema.tasks.dependsOn.string", @@ -11882,38 +12623,18 @@ "TaskTypeConfiguration.noType", "TaskDefinitionExtPoint" ], - "vs/workbench/contrib/tasks/common/jsonSchema_v1": [ - "JsonSchema.version.deprecated", - "JsonSchema.version", - "JsonSchema._runner", - "JsonSchema.runner", - "JsonSchema.windows", - "JsonSchema.mac", - "JsonSchema.linux", - "JsonSchema.shell" - ], "vs/workbench/contrib/remote/browser/tunnelFactory": [ "tunnelPrivacy.private", "tunnelPrivacy.public" ], "vs/workbench/contrib/remote/browser/remote": [ - "getStartedWalkthrough.id", - "RemoteHelpInformationExtPoint", - "RemoteHelpInformationExtPoint.getStarted", - "RemoteHelpInformationExtPoint.documentation", - "RemoteHelpInformationExtPoint.feedback", - "RemoteHelpInformationExtPoint.feedback.deprecated", - "RemoteHelpInformationExtPoint.reportIssue", - "RemoteHelpInformationExtPoint.issues", "remote.help.getStarted", "remote.help.documentation", "remote.help.issues", "remote.help.report", "pickRemoteExtension", - "remote.help", "remotehelp", "remote.explorer", - "remote.explorer", "reconnectionWaitOne", "reconnectionWaitMany", "reconnectNow", @@ -11926,28 +12647,17 @@ "comment": [ "&& denotes a mnemonic" ] - } - ], - "vs/workbench/contrib/emmet/browser/actions/expandAbbreviation": [ - "expandAbbreviationAction", - { - "key": "miEmmetExpandAbbreviation", - "comment": [ - "&& denotes a mnemonic" - ] - } + }, + "remote.help", + "remote.explorer" ], "vs/workbench/contrib/remote/browser/remoteIndicator": [ - "remote.category", - "remote.showMenu", - "remote.close", { "key": "miCloseRemote", "comment": [ "&& denotes a mnemonic" ] }, - "remote.install", "host.open", "host.open", "host.reconnecting", @@ -11980,109 +12690,54 @@ "reloadWindow", "closeVirtualWorkspace.title", "remoteActions", - "remote.startActions.installingExtension" + "remote.startActions.installingExtension", + "remote.showExtensionRecommendations", + "remote.category", + "remote.showMenu", + "remote.close", + "remote.install" ], - "vs/workbench/contrib/format/browser/formatActionsNone": [ - "formatDocument.label.multiple", - "too.large", - "no.provider", + "vs/workbench/contrib/remote/browser/remoteConnectionHealth": [ + "unsupportedGlibcWarning", { - "key": "install.formatter", + "key": "allow", "comment": [ "&& denotes a mnemonic" ] - } - ], - "vs/workbench/contrib/format/browser/formatModified": [ - "formatChanges" - ], - "vs/workbench/contrib/snippets/browser/commands/configureSnippets": [ - "global.scope", - "global.1", - "detail.label", - "name", - "bad_name1", - "bad_name2", - "bad_name3", - "openSnippet.label", - "userSnippets", + }, { - "key": "miOpenSnippets", + "key": "learnMore", "comment": [ "&& denotes a mnemonic" ] }, - "new.global_scope", - "new.global", - "new.workspace_scope", - "new.folder", - "group.global", - "new.global.sep", - "new.global.sep", - "openSnippet.pickLanguage" + "remember", + "unsupportedGlibcBannerLearnMore", + "unsupportedGlibcWarning.banner" ], - "vs/workbench/contrib/format/browser/formatActionsMultiple": [ - "null", - "nullFormatterDescription", - "miss", - "config.needed", - "config.bad", - "miss.1", + "vs/workbench/contrib/emmet/browser/actions/expandAbbreviation": [ + "expandAbbreviationAction", { - "key": "do.config", + "key": "miEmmetExpandAbbreviation", "comment": [ "&& denotes a mnemonic" ] - }, - "do.config.notification", - "select", - "do.config.command", - "summary", - "formatter", - "formatter.default", - "def", - "config", - "format.placeHolder", - "select", - "formatDocument.label.multiple", - "formatSelection.label.multiple" - ], - "vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets": [ - "label", - "placeholder" - ], - "vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet": [ - "label" - ], - "vs/workbench/contrib/snippets/browser/commands/insertSnippet": [ - "snippet.suggestions.label" - ], - "vs/workbench/contrib/snippets/browser/snippetCodeActionProvider": [ - "codeAction", - "overflow.start.title", - "title" - ], - "vs/workbench/contrib/snippets/browser/snippetsService": [ - "invalid.path.0", - "invalid.language.0", - "invalid.language", - "invalid.path.1", - "vscode.extension.contributes.snippets", - "vscode.extension.contributes.snippets-language", - "vscode.extension.contributes.snippets-path", - "badVariableUse", - "badFile" + } ], "vs/workbench/contrib/codeEditor/browser/accessibility/accessibility": [ - "toggleScreenReaderMode" + "toggleScreenReaderMode", + "toggleScreenReaderModeDescription" ], "vs/workbench/contrib/codeEditor/browser/diffEditorHelper": [ "hintWhitespace", "hintTimeout", "removeTimeout", + "msg3", + "switchSidesNoKb", + "msg5", "msg1", "msg2", - "msg3" + "msg4" ], "vs/workbench/contrib/codeEditor/browser/inspectKeybindings": [ "workbench.action.inspectKeyMap", @@ -12103,9 +12758,9 @@ "inspectTMScopesWidget.loading" ], "vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess": [ - "gotoLine", "gotoLineQuickAccessPlaceholder", - "gotoLineQuickAccess" + "gotoLineQuickAccess", + "gotoLine" ], "vs/workbench/contrib/codeEditor/browser/saveParticipants": [ { @@ -12124,46 +12779,46 @@ "codeAction.apply" ], "vs/workbench/contrib/codeEditor/browser/toggleColumnSelection": [ - "toggleColumnSelection", { "key": "miColumnSelection", "comment": [ "&& denotes a mnemonic" ] - } + }, + "toggleColumnSelection" ], "vs/workbench/contrib/codeEditor/browser/toggleMinimap": [ - "toggleMinimap", { "key": "miMinimap", "comment": [ "&& denotes a mnemonic" ] - } + }, + "toggleMinimap" ], - "vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace": [ - "toggleRenderWhitespace", + "vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter": [ { - "key": "miToggleRenderWhitespace", + "key": "miToggleRenderControlCharacters", "comment": [ "&& denotes a mnemonic" ] - } + }, + "toggleRenderControlCharacters" ], "vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier": [ - "toggleLocation", "miMultiCursorAlt", "miMultiCursorCmd", - "miMultiCursorCtrl" + "miMultiCursorCtrl", + "toggleLocation" ], - "vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter": [ - "toggleRenderControlCharacters", + "vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace": [ { - "key": "miToggleRenderControlCharacters", + "key": "miToggleRenderWhitespace", "comment": [ "&& denotes a mnemonic" ] - } + }, + "toggleRenderWhitespace" ], "vs/workbench/contrib/codeEditor/browser/toggleWordWrap": [ "editorWordWrap", @@ -12196,83 +12851,206 @@ "defaultHintAriaLabel", "disableHint" ], - "vs/workbench/contrib/update/browser/update": [ - "update.noReleaseNotesOnline", - "read the release notes", - "releaseNotes", - "update service disabled", - "learn more", - "updateIsReady", - "checkingForUpdates", - "downloading", - "updating", - "update service", - "noUpdatesAvailable", - "thereIsUpdateAvailable", - "download update", - "later", - "releaseNotes", - "updateAvailable", - "installUpdate", - "later", - "releaseNotes", - "updateNow", - "later", - "releaseNotes", - "updateAvailableAfterRestart", - "checkForUpdates", - "checkingForUpdates", - "download update_1", - "DownloadingUpdate", - "installUpdate...", - "installingUpdate", - "showUpdateReleaseNotes", - "restartToUpdate", - "switchToInsiders", - "switchToStable", - "relaunchMessage", - "relaunchDetailInsiders", - "relaunchDetailStable", + "vs/workbench/contrib/codeEditor/browser/dictation/editorDictation": [ + "stopDictationShort1", + "stopDictationShort2", + "voiceCategory", + "startDictation", + "stopDictation" + ], + "vs/platform/history/browser/contextScopedHistoryWidget": [ + "suggestWidgetVisible" + ], + "vs/workbench/contrib/debug/browser/linkDetector": [ + "followForwardedLink", + "followLink", + "fileLinkWithPathMac", + "fileLinkWithPath", + "fileLinkMac", + "fileLink" + ], + "vs/workbench/contrib/debug/browser/replViewer": [ + "debugConsole", + "replVariableAriaLabel", { - "key": "reload", + "key": "occurred", "comment": [ - "&& denotes a mnemonic" + "Front will the value of the debug console element. Placeholder will be replaced by a number which represents occurrance count." ] }, - "selectSyncService.message", - "selectSyncService.detail", + "replRawObjectAriaLabel", + "replGroup" + ], + "vs/workbench/contrib/debug/common/replModel": [ + "consoleCleared" + ], + "vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet": [ + "label" + ], + "vs/workbench/contrib/snippets/browser/commands/configureSnippets": [ + "global.scope", + "global.1", + "detail.label", + "name", + "bad_name1", + "bad_name2", + "bad_name3", { - "key": "use insiders", + "key": "miOpenSnippets", "comment": [ "&& denotes a mnemonic" ] }, - { - "key": "use stable", - "comment": [ - "&& denotes a mnemonic" - ] - } - ], - "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput": [ - "getStarted" + "new.global_scope", + "new.global", + "new.workspace_scope", + "new.folder", + "group.global", + "new.global.sep", + "new.global.sep", + "openSnippet.pickLanguage", + "openSnippet.label", + "userSnippets" ], - "vs/workbench/contrib/welcomeGettingStarted/browser/startupPage": [ - "welcome.displayName", - "startupPage.markdownPreviewError" + "vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets": [ + "placeholder", + "label" ], - "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedIcons": [ - "gettingStartedUnchecked", - "gettingStartedChecked" + "vs/workbench/contrib/snippets/browser/snippetCodeActionProvider": [ + "more", + "codeAction", + "overflow.start.title", + "title" + ], + "vs/workbench/contrib/snippets/browser/commands/insertSnippet": [ + "snippet.suggestions.label" + ], + "vs/workbench/contrib/snippets/browser/snippetsService": [ + "invalid.path.0", + "invalid.language.0", + "invalid.language", + "invalid.path.1", + "vscode.extension.contributes.snippets", + "vscode.extension.contributes.snippets-language", + "vscode.extension.contributes.snippets-path", + "badVariableUse", + "badFile" + ], + "vs/workbench/contrib/format/browser/formatActionsNone": [ + "formatDocument.label.multiple", + "too.large", + "no.provider", + { + "key": "install.formatter", + "comment": [ + "&& denotes a mnemonic" + ] + } + ], + "vs/workbench/contrib/format/browser/formatActionsMultiple": [ + "null", + "nullFormatterDescription", + "miss.1", + "miss.2", + "config.needed", + "config.bad", + "miss", + { + "key": "do.config", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "do.config.notification", + "select", + "do.config.command", + "summary", + "formatter", + "formatter.default", + "def", + "config", + "format.placeHolder", + "select", + "formatDocument.label.multiple", + "formatSelection.label.multiple" + ], + "vs/workbench/contrib/format/browser/formatModified": [ + "formatChanges" + ], + "vs/workbench/contrib/update/browser/update": [ + "update.noReleaseNotesOnline", + "read the release notes", + "releaseNotes", + "update service disabled", + "learn more", + "updateIsReady", + "checkingForUpdates", + "downloading", + "updating", + "update service", + "noUpdatesAvailable", + "thereIsUpdateAvailable", + "download update", + "later", + "releaseNotes", + "updateAvailable", + "installUpdate", + "later", + "releaseNotes", + "updateNow", + "later", + "releaseNotes", + "updateAvailableAfterRestart", + "checkForUpdates", + "checkingForUpdates", + "download update_1", + "DownloadingUpdate", + "installUpdate...", + "installingUpdate", + "showUpdateReleaseNotes", + "restartToUpdate", + "switchToInsiders", + "switchToStable", + "relaunchMessage", + "relaunchDetailInsiders", + "relaunchDetailStable", + { + "key": "reload", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "selectSyncService.message", + "selectSyncService.detail", + { + "key": "use insiders", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "use stable", + "comment": [ + "&& denotes a mnemonic" + ] + } ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService": [ "builtin", "developer", - "resetWelcomePageWalkthroughProgress" + "resetWelcomePageWalkthroughProgress", + "resetGettingStartedProgressDescription" ], - "vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart": [ - "walkThrough.unboundCommand", - "walkThrough.gitNotFound" + "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput": [ + "getStarted" + ], + "vs/workbench/contrib/welcomeGettingStarted/browser/startupPage": [ + "welcome.displayName", + "startupPage.markdownPreviewError" + ], + "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedIcons": [ + "gettingStartedUnchecked", + "gettingStartedChecked" ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted": [ "welcomeAriaLabel", @@ -12307,6 +13085,9 @@ "showAll", "close", "closeAriaLabel", + "videos", + "videos-title", + "videos-description", "gettingStarted.allStepsComplete", "gettingStarted.someStepsComplete", "gettingStarted.keyboardTip", @@ -12324,7 +13105,12 @@ ], "vs/workbench/contrib/welcomeWalkthrough/browser/editor/editorWalkThrough": [ "editorWalkThrough.title", - "editorWalkThrough" + "editorWalkThrough", + "editorWalkThroughMetadata" + ], + "vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart": [ + "walkThrough.unboundCommand", + "walkThrough.gitNotFound" ], "vs/workbench/contrib/welcomeViews/common/viewsWelcomeContribution": [ "ViewsWelcomeExtensionPoint.proposedAPI" @@ -12339,11 +13125,12 @@ "contributes.viewsWelcome.view.group", "contributes.viewsWelcome.view.enablement" ], - "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree": [ - "title.template", - "1.problem", - "N.problem", - "deep.problem" + "vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek": [ + "callFrom", + "callsTo", + "title.loading", + "empt.callsFrom", + "empt.callsTo" ], "vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek": [ "supertypes", @@ -12352,12 +13139,11 @@ "empt.supertypes", "empt.subtypes" ], - "vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek": [ - "callFrom", - "callsTo", - "title.loading", - "empt.callsFrom", - "empt.callsTo" + "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree": [ + "title.template", + "1.problem", + "N.problem", + "deep.problem" ], "vs/workbench/contrib/outline/browser/outlinePane": [ "no-editor", @@ -12373,50 +13159,38 @@ "sortByName", "sortByKind" ], - "vs/workbench/contrib/userDataProfile/browser/userDataProfileActions": [ - "create temporary profile", - "rename profile", - "select profile to rename", - "profileExists", - "current", - "rename specific profile", - "pick profile to rename", - "mange", - "cleanup profile", - "reset workspaces" + "vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction": [ + "signOutOfAccount", + "signOutMessage", + "signOutMessageSimple", + { + "key": "signOut", + "comment": [ + "&& denotes a mnemonic" + ] + } ], - "vs/workbench/contrib/userDataProfile/browser/userDataProfile": [ - "profiles", - "switchProfile", - "selectProfile", - "edit profile", - "show profile contents", - "export profile", - "export profile in share", - "import profile", - "import from url", - "import from file", - "templates", - "import profile quick pick title", - "import profile placeholder", - "profile import error", - "import profile dialog", - "import profile share", - "save profile as", - "create profile", - "delete profile", - "current", - "delete specific profile", - "pick profile to delete" + "vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction": [ + "pickAccount", + "noTrustedExtensions", + "manageTrustedExtensions.cancel", + { + "key": "accountLastUsedDate", + "comment": [ + "The placeholder {0} is a string with time information, such as \"3 days ago\"" + ] + }, + "notUsed", + "trustedExtensionTooltip", + "trustedExtensions", + "manageTrustedExtensions", + "manageExtensions", + "manageTrustedExtensionsForAccount", + "accounts" ], "vs/workbench/contrib/userDataSync/browser/userDataSync": [ - "stop sync", - "configure sync", - "sync now", "syncing", "synced with time", - "sync settings", - "show synced data", "conflicts detected", "replace remote", "replace local", @@ -12483,12 +13257,10 @@ "default", "insiders", "stable", - "global activity turn on sync", "turnin on sync", "cancel turning on sync", "sign in global", "sign in accounts", - "resolveConflicts_global", "sync is on", "turn off failed", "configure", @@ -12496,50 +13268,38 @@ "show sync log toolrip", "complete merges title", "download sync activity complete", - "workbench.actions.syncData.reset" - ], - "vs/workbench/contrib/editSessions/common/editSessions": [ - "cloud changes", - "cloud changes", - "editSessionViewIcon" + "workbench.actions.syncData.reset", + "stop sync", + "configure sync", + "sync now", + "sync settings", + "show synced data", + "global activity turn on sync", + "resolveConflicts_global" ], - "vs/workbench/contrib/editSessions/common/editSessionsLogService": [ - "cloudChangesLog" + "vs/workbench/contrib/userDataProfile/browser/userDataProfileActions": [ + "select profile to rename", + "profileExists", + "current", + "rename specific profile", + "pick profile to rename", + "create temporary profile", + "rename profile", + "mange", + "cleanup profile", + "reset workspaces" ], - "vs/workbench/contrib/editSessions/browser/editSessionsStorageService": [ - "choose account read placeholder", - "choose account placeholder", - "signed in", - "others", - "sign in using account", - "sign in", - "sign in badge", - "reset auth.v3", - "sign out of cloud changes clear data prompt", - "delete all cloud changes" - ], - "vs/workbench/contrib/editSessions/browser/editSessionsViews": [ - "noStoredChanges", - "storeWorkingChangesTitle", - "workbench.editSessions.actions.resume.v2", - "workbench.editSessions.actions.store.v2", - "workbench.editSessions.actions.delete.v2", - "confirm delete.v2", - "confirm delete detail.v2", - "workbench.editSessions.actions.deleteAll", - "confirm delete all", - "confirm delete all detail", - "compare changes", - "local copy", - "cloud changes", - "open file" - ], - "vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint": [ - "contributes.codeActions", - "contributes.codeActions.languages", - "contributes.codeActions.kind", - "contributes.codeActions.title", - "contributes.codeActions.description" + "vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint": [ + "contributes.codeActions", + "contributes.codeActions.languages", + "contributes.codeActions.kind", + "contributes.codeActions.title", + "contributes.codeActions.description", + "codeActions.title", + "codeActions.kind", + "codeActions.description", + "codeActions.languages", + "codeactions" ], "vs/workbench/contrib/codeActions/common/documentationExtensionPoint": [ "contributes.documentation", @@ -12549,20 +13309,9 @@ "contributes.documentation.refactoring.when", "contributes.documentation.refactoring.command" ], - "vs/workbench/contrib/codeActions/browser/codeActionsContribution": [ - "alwaysSave", - "explicitSave", - "neverSave", - "explicitSaveBoolean", - "neverSaveBoolean", - "codeActionsOnSave.fixAll", - "editor.codeActionsOnSave", - "codeActionsOnSave.generic" - ], "vs/workbench/contrib/timeline/browser/timelinePane": [ "timeline.loadingMore", "timeline.loadMore", - "timeline", "timeline.editorCannotProvideTimeline", "timeline.noTimelineSourcesEnabled", "timeline.noLocalHistoryYet", @@ -12576,6 +13325,7 @@ "timelineRefresh", "timelinePin", "timelineUnpin", + "timeline", "refresh", "timeline", "timeline.toggleFollowActiveEditorCommand.follow", @@ -12583,17 +13333,44 @@ "timeline.toggleFollowActiveEditorCommand.unfollow", "timeline" ], + "vs/workbench/contrib/codeActions/browser/codeActionsContribution": [ + "alwaysSave", + "explicitSave", + "neverSave", + "explicitSaveBoolean", + "neverSaveBoolean", + "codeActionsOnSave.fixAll", + "editor.codeActionsOnSave", + "codeActionsOnSave.generic" + ], "vs/workbench/contrib/localHistory/browser/localHistoryTimeline": [ "localHistory" ], + "vs/workbench/contrib/userDataProfile/browser/userDataProfile": [ + "profiles", + "selectProfile", + "import from url", + "import from file", + "templates", + "import profile quick pick title", + "import profile placeholder", + "profile import error", + "import profile dialog", + "current", + "delete specific profile", + "pick profile to delete", + "switchProfile", + "edit profile", + "show profile contents", + "export profile", + "export profile in share", + "import profile", + "import profile share", + "save profile as", + "create profile", + "delete profile" + ], "vs/workbench/contrib/localHistory/browser/localHistoryCommands": [ - "localHistory.category", - "localHistory.compareWithFile", - "localHistory.compareWithPrevious", - "localHistory.selectForCompare", - "localHistory.compareWithSelected", - "localHistory.open", - "localHistory.restore", "localHistoryRestore.source", "confirmRestoreMessage", "confirmRestoreDetail", @@ -12604,14 +13381,10 @@ ] }, "unableToRestore", - "localHistory.restoreViaPicker", "restoreViaPicker.filePlaceholder", "restoreViaPicker.entryPlaceholder", - "localHistory.restoreViaPickerMenu", - "localHistory.rename", "renameLocalHistoryEntryTitle", "renameLocalHistoryPlaceholder", - "localHistory.delete", "confirmDeleteMessage", "confirmDeleteDetail", { @@ -12620,7 +13393,6 @@ "&& denotes a mnemonic" ] }, - "localHistory.deleteAll", "confirmDeleteAllMessage", "confirmDeleteAllDetail", { @@ -12629,22 +13401,76 @@ "&& denotes a mnemonic" ] }, - "localHistory.create", "createLocalHistoryEntryTitle", "createLocalHistoryPlaceholder", "localHistoryEditorLabel", "localHistoryCompareToFileEditorLabel", - "localHistoryCompareToPreviousEditorLabel" + "localHistoryCompareToPreviousEditorLabel", + "localHistory.category", + "localHistory.compareWithFile", + "localHistory.compareWithPrevious", + "localHistory.selectForCompare", + "localHistory.compareWithSelected", + "localHistory.open", + "localHistory.restore", + "localHistory.restoreViaPicker", + "localHistory.restoreViaPickerMenu", + "localHistory.rename", + "localHistory.delete", + "localHistory.deleteAll", + "localHistory.create" + ], + "vs/workbench/contrib/editSessions/common/editSessions": [ + "editSessionViewIcon", + "cloud changes", + "cloud changes" + ], + "vs/workbench/contrib/editSessions/browser/editSessionsStorageService": [ + "choose account read placeholder", + "choose account placeholder", + "signed in", + "others", + "sign in using account", + "sign in", + "sign in badge", + "reset auth.v3", + "sign out of cloud changes clear data prompt", + "delete all cloud changes" + ], + "vs/workbench/contrib/editSessions/common/editSessionsLogService": [ + "cloudChangesLog" + ], + "vs/workbench/contrib/editSessions/browser/editSessionsViews": [ + "noStoredChanges", + "storeWorkingChangesTitle", + "workbench.editSessions.actions.resume.v2", + "workbench.editSessions.actions.store.v2", + "workbench.editSessions.actions.delete.v2", + "confirm delete.v2", + "confirm delete detail.v2", + "workbench.editSessions.actions.deleteAll", + "confirm delete all", + "confirm delete all detail", + "compare changes", + "local copy", + "cloud changes", + "open file" + ], + "vs/workbench/contrib/accessibilitySignals/browser/commands": [ + "accessibility.sound.help.description", + "sounds.help.settings", + "sounds.help.placeholder", + "accessibility.announcement.help.description", + "announcement.help.settings", + "announcement.help.placeholder", + "announcement.help.placeholder.disabled", + "signals.sound.help", + "accessibility.announcement.help" ], "vs/workbench/services/workspaces/browser/workspaceTrustEditorInput": [ + "workspaceTrustEditorLabelIcon", "workspaceTrustEditorInputName" ], - "vs/workbench/contrib/audioCues/browser/commands": [ - "audioCues.help", - "disabled", - "audioCues.help.settings", - "audioCues.help.placeholder" - ], "vs/workbench/contrib/workspace/browser/workspaceTrustEditor": [ "shieldIcon", "checkListIcon", @@ -12749,22 +13575,6 @@ "untrustedFolderReason", "trustedForcedReason" ], - "vs/workbench/contrib/accessibility/browser/accessibilityConfiguration": [ - "accessibilityConfigurationTitle", - "verbosity.terminal.description", - "verbosity.diffEditor.description", - "verbosity.chat.description", - "verbosity.interactiveEditor.description", - "verbosity.inlineCompletions.description", - "verbosity.keybindingsEditor.description", - "verbosity.notebook", - "verbosity.hover", - "verbosity.notification", - "verbosity.emptyEditorHint", - "verbosity.comments", - "dimUnfocusedEnabled", - "dimUnfocusedOpacity" - ], "vs/workbench/contrib/accessibility/browser/accessibilityStatus": [ "screenReaderDetectedExplanation.question", "screenReaderDetectedExplanation.answerYes", @@ -12772,14 +13582,155 @@ "screenReaderDetected", "status.editor.screenReaderMode" ], + "vs/workbench/contrib/comments/browser/commentsAccessibility": [ + "intro", + "introWidget", + "introWidgetNoKb", + "commentCommands", + "escape", + "next", + "nextNoKb", + "previous", + "previousNoKb", + "nextCommentThreadKb", + "nextCommentThreadNoKb", + "previousCommentThreadKb", + "previousCommentThreadNoKb", + "addComment", + "addCommentNoKb", + "submitComment", + "submitCommentNoKb" + ], + "vs/workbench/contrib/accessibilitySignals/browser/openDiffEditorAnnouncement": [ + "openDiffEditorAnnouncement" + ], + "vs/workbench/contrib/accessibility/browser/audioCueConfiguration": [ + "audioCues.enabled.auto", + "audioCues.enabled.on", + "audioCues.enabled.off", + "audioCues.enabled.deprecated", + "audioCues.debouncePositionChanges", + "audioCues.debouncePositionChangesDeprecated", + "audioCues.lineHasBreakpoint", + "audioCues.lineHasInlineSuggestion", + "audioCues.lineHasError", + "audioCues.lineHasFoldedArea", + "audioCues.lineHasWarning", + "audioCues.onDebugBreak", + "audioCues.noInlayHints", + "audioCues.taskCompleted", + "audioCues.taskFailed", + "audioCues.terminalCommandFailed", + "audioCues.terminalQuickFix", + "audioCues.terminalBell", + "audioCues.diffLineInserted", + "audioCues.diffLineDeleted", + "audioCues.diffLineModified", + "audioCues.notebookCellCompleted", + "audioCues.notebookCellFailed", + "audioCues.chatRequestSent", + "audioCues.chatResponsePending", + "audioCues.chatResponseReceived", + "audioCues.clear", + "audioCues.save", + "audioCues.save.userGesture", + "audioCues.save.always", + "audioCues.save.never", + "audioCues.format", + "audioCues.format.userGesture", + "audioCues.format.always", + "audioCues.format.never" + ], + "vs/workbench/contrib/scrollLocking/browser/scrollLocking": [ + "mouseScrolllingLocked", + "mouseLockScrollingEnabled", + { + "key": "miToggleLockedScrolling", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "synchronizeScrolling", + { + "key": "miHoldLockedScrolling", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "toggleLockedScrolling", + "holdLockedScrolling" + ], "vs/workbench/contrib/share/browser/shareService": [ "shareProviderCount", "type to filter" ], + "vs/workbench/browser/window": [ + "quitMessageMac", + "quitMessage", + "closeWindowMessage", + { + "key": "quitButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "exitButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "closeWindowButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "doNotAskAgain", + "shutdownError", + "shutdownErrorDetail", + { + "key": "reload", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "unableToOpenExternal", + { + "key": "open", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "learnMore", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "openExternalDialogButtonRetry.v2", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "openExternalDialogDetail.v2", + { + "key": "openExternalDialogButtonInstall.v3", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "openExternalDialogDetailNoInstall", + "openExternalDialogTitle" + ], "vs/workbench/browser/parts/notifications/notificationsCenter": [ "notificationsEmpty", "notifications", "notificationsToolbar", + "turnOnNotifications", + "turnOffNotifications", + "moreSources", "notificationsCenterWidgetAriaLabel" ], "vs/workbench/browser/parts/notifications/notificationsStatus": [ @@ -12817,24 +13768,26 @@ }, "status.message" ], + "vs/workbench/browser/parts/notifications/notificationsAlerts": [ + "alertErrorMessage", + "alertWarningMessage", + "alertInfoMessage" + ], "vs/workbench/browser/parts/notifications/notificationsCommands": [ + "selectSources", "notifications", "showNotifications", "hideNotifications", "clearAllNotifications", "acceptNotificationPrimaryAction", "toggleDoNotDisturbMode", + "toggleDoNotDisturbModeBySource", "focusNotificationToasts" ], "vs/workbench/browser/parts/notifications/notificationsToasts": [ "notificationAriaLabel", "notificationWithSourceAriaLabel" ], - "vs/workbench/browser/parts/notifications/notificationsAlerts": [ - "alertErrorMessage", - "alertWarningMessage", - "alertInfoMessage" - ], "vs/workbench/services/configuration/common/configurationEditing": [ "fsError", "openTasksConfiguration", @@ -12879,9 +13832,6 @@ "workspaceTarget", "folderTarget" ], - "vs/workbench/common/editor/textEditorModel": [ - "languageAutoDetected" - ], "vs/workbench/services/textfile/common/textFileEditorModelManager": [ { "key": "genericSaveError", @@ -12890,10 +13840,256 @@ ] } ], + "vs/workbench/common/editor/textEditorModel": [ + "languageAutoDetected" + ], + "vs/platform/theme/common/colors/chartsColors": [ + "chartsForeground", + "chartsLines", + "chartsRed", + "chartsBlue", + "chartsYellow", + "chartsOrange", + "chartsGreen", + "chartsPurple" + ], + "vs/platform/theme/common/colors/inputColors": [ + "inputBoxBackground", + "inputBoxForeground", + "inputBoxBorder", + "inputBoxActiveOptionBorder", + "inputOption.hoverBackground", + "inputOption.activeBackground", + "inputOption.activeForeground", + "inputPlaceholderForeground", + "inputValidationInfoBackground", + "inputValidationInfoForeground", + "inputValidationInfoBorder", + "inputValidationWarningBackground", + "inputValidationWarningForeground", + "inputValidationWarningBorder", + "inputValidationErrorBackground", + "inputValidationErrorForeground", + "inputValidationErrorBorder", + "dropdownBackground", + "dropdownListBackground", + "dropdownForeground", + "dropdownBorder", + "buttonForeground", + "buttonSeparator", + "buttonBackground", + "buttonHoverBackground", + "buttonBorder", + "buttonSecondaryForeground", + "buttonSecondaryBackground", + "buttonSecondaryHoverBackground", + "checkbox.background", + "checkbox.select.background", + "checkbox.foreground", + "checkbox.border", + "checkbox.select.border", + "keybindingLabelBackground", + "keybindingLabelForeground", + "keybindingLabelBorder", + "keybindingLabelBottomBorder" + ], + "vs/platform/theme/common/colors/editorColors": [ + "editorBackground", + "editorForeground", + "editorStickyScrollBackground", + "editorStickyScrollHoverBackground", + "editorStickyScrollBorder", + "editorStickyScrollShadow", + "editorWidgetBackground", + "editorWidgetForeground", + "editorWidgetBorder", + "editorWidgetResizeBorder", + "editorError.background", + "editorError.foreground", + "errorBorder", + "editorWarning.background", + "editorWarning.foreground", + "warningBorder", + "editorInfo.background", + "editorInfo.foreground", + "infoBorder", + "editorHint.foreground", + "hintBorder", + "activeLinkForeground", + "editorSelectionBackground", + "editorSelectionForeground", + "editorInactiveSelection", + "editorSelectionHighlight", + "editorSelectionHighlightBorder", + "editorFindMatch", + "findMatchHighlight", + "findRangeHighlight", + "editorFindMatchBorder", + "findMatchHighlightBorder", + "findRangeHighlightBorder", + "hoverHighlight", + "hoverBackground", + "hoverForeground", + "hoverBorder", + "statusBarBackground", + "editorInlayHintForeground", + "editorInlayHintBackground", + "editorInlayHintForegroundTypes", + "editorInlayHintBackgroundTypes", + "editorInlayHintForegroundParameter", + "editorInlayHintBackgroundParameter", + "editorLightBulbForeground", + "editorLightBulbAutoFixForeground", + "editorLightBulbAiForeground", + "snippetTabstopHighlightBackground", + "snippetTabstopHighlightBorder", + "snippetFinalTabstopHighlightBackground", + "snippetFinalTabstopHighlightBorder", + "diffEditorInserted", + "diffEditorRemoved", + "diffEditorInsertedLines", + "diffEditorRemovedLines", + "diffEditorInsertedLineGutter", + "diffEditorRemovedLineGutter", + "diffEditorOverviewInserted", + "diffEditorOverviewRemoved", + "diffEditorInsertedOutline", + "diffEditorRemovedOutline", + "diffEditorBorder", + "diffDiagonalFill", + "diffEditor.unchangedRegionBackground", + "diffEditor.unchangedRegionForeground", + "diffEditor.unchangedCodeBackground", + "widgetShadow", + "widgetBorder", + "toolbarHoverBackground", + "toolbarHoverOutline", + "toolbarActiveBackground", + "breadcrumbsFocusForeground", + "breadcrumbsBackground", + "breadcrumbsFocusForeground", + "breadcrumbsSelectedForeground", + "breadcrumbsSelectedBackground", + "mergeCurrentHeaderBackground", + "mergeCurrentContentBackground", + "mergeIncomingHeaderBackground", + "mergeIncomingContentBackground", + "mergeCommonHeaderBackground", + "mergeCommonContentBackground", + "mergeBorder", + "overviewRulerCurrentContentForeground", + "overviewRulerIncomingContentForeground", + "overviewRulerCommonContentForeground", + "overviewRulerFindMatchForeground", + "overviewRulerSelectionHighlightForeground", + "problemsErrorIconForeground", + "problemsWarningIconForeground", + "problemsInfoIconForeground" + ], + "vs/platform/theme/common/colors/minimapColors": [ + "minimapFindMatchHighlight", + "minimapSelectionOccurrenceHighlight", + "minimapSelectionHighlight", + "minimapInfo", + "overviewRuleWarning", + "minimapError", + "minimapBackground", + "minimapForegroundOpacity", + "minimapSliderBackground", + "minimapSliderHoverBackground", + "minimapSliderActiveBackground" + ], + "vs/platform/theme/common/colors/listColors": [ + "listFocusBackground", + "listFocusForeground", + "listFocusOutline", + "listFocusAndSelectionOutline", + "listActiveSelectionBackground", + "listActiveSelectionForeground", + "listActiveSelectionIconForeground", + "listInactiveSelectionBackground", + "listInactiveSelectionForeground", + "listInactiveSelectionIconForeground", + "listInactiveFocusBackground", + "listInactiveFocusOutline", + "listHoverBackground", + "listHoverForeground", + "listDropBackground", + "listDropBetweenBackground", + "highlight", + "listFocusHighlightForeground", + "invalidItemForeground", + "listErrorForeground", + "listWarningForeground", + "listFilterWidgetBackground", + "listFilterWidgetOutline", + "listFilterWidgetNoMatchesOutline", + "listFilterWidgetShadow", + "listFilterMatchHighlight", + "listFilterMatchHighlightBorder", + "listDeemphasizedForeground", + "treeIndentGuidesStroke", + "treeInactiveIndentGuidesStroke", + "tableColumnsBorder", + "tableOddRowsBackgroundColor" + ], + "vs/platform/theme/common/colors/miscColors": [ + "sashActiveBorder", + "badgeBackground", + "badgeForeground", + "scrollbarShadow", + "scrollbarSliderBackground", + "scrollbarSliderHoverBackground", + "scrollbarSliderActiveBackground", + "progressBarBackground" + ], + "vs/platform/theme/common/colors/menuColors": [ + "menuBorder", + "menuForeground", + "menuBackground", + "menuSelectionForeground", + "menuSelectionBackground", + "menuSelectionBorder", + "menuSeparatorBackground" + ], + "vs/platform/theme/common/colors/baseColors": [ + "foreground", + "disabledForeground", + "errorForeground", + "descriptionForeground", + "iconForeground", + "focusBorder", + "contrastBorder", + "activeContrastBorder", + "selectionBackground", + "textLinkForeground", + "textLinkActiveForeground", + "textSeparatorForeground", + "textPreformatForeground", + "textPreformatBackground", + "textBlockQuoteBackground", + "textBlockQuoteBorder", + "textCodeBlockBackground" + ], + "vs/platform/theme/common/colors/quickpickColors": [ + "pickerBackground", + "pickerForeground", + "pickerTitleBackground", + "pickerGroupForeground", + "pickerGroupBorder", + "quickInput.list.focusBackground deprecation", + "quickInput.listFocusForeground", + "quickInput.listFocusIconForeground", + "quickInput.listFocusBackground" + ], + "vs/platform/theme/common/colors/searchColors": [ + "search.resultsInfoForeground", + "searchEditor.queryMatch", + "searchEditor.editorFindMatchBorder" + ], "vs/workbench/browser/parts/titlebar/titlebarPart": [ - "focusTitleBar", - "toggle.commandCenter", - "toggle.layout" + "ariaLabelTitleActions", + "focusTitleBar" ], "vs/workbench/services/configurationResolver/common/variableResolver": [ "canNotResolveFile", @@ -12915,25 +14111,23 @@ "vs/workbench/services/workingCopy/common/workingCopyHistoryTracker": [ "undoRedo.source" ], - "vs/workbench/services/extensions/common/extensionsUtil": [ - "overwritingExtension", - "overwritingExtension", - "extensionUnderDevelopment" - ], "vs/workbench/services/extensions/common/extensionHostManager": [ "measureExtHostLatency" ], - "vs/workbench/contrib/extensions/common/reportExtensionIssueAction": [ - "reportExtensionIssue" - ], "vs/workbench/contrib/localization/common/localizationsActions": [ - "configureLocale", "chooseLocale", "installed", "available", "moreInfo", + "configureLocale", + "configureLocaleDescription", "clearDisplayLanguage" ], + "vs/workbench/services/extensions/common/extensionsUtil": [ + "overwritingExtension", + "overwritingExtension", + "extensionUnderDevelopment" + ], "vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions": [ "cmd.reportOrShow", "cmd.report", @@ -12943,33 +14137,61 @@ "attach.title", "attach.msg2" ], + "vs/workbench/contrib/extensions/common/reportExtensionIssueAction": [ + "reportExtensionIssue" + ], "vs/workbench/contrib/terminal/electron-sandbox/terminalRemote": [ "workbench.action.terminal.newLocal" ], - "vs/workbench/contrib/tasks/common/taskTemplates": [ - "dotnetCore", - "msbuild", - "externalCommand", - "Maven" + "vs/workbench/contrib/terminal/browser/baseTerminalBackend": [ + "ptyHostStatus", + "ptyHostStatus.short", + "nonResponsivePtyHost", + "ptyHostStatus.ariaLabel" ], - "vs/workbench/contrib/tasks/browser/taskQuickPick": [ - "taskQuickPick.showAll", - "configureTaskIcon", - "removeTaskIcon", - "configureTask", - "contributedTasks", - "taskType", - "removeRecent", - "recentlyUsed", - "configured", - "configured", - "TaskQuickPick.changeSettingDetails", - "TaskQuickPick.changeSettingNo", - "TaskService.pickRunTask", - "TaskQuickPick.changeSettingsOptions", - "TaskQuickPick.goBack", - "TaskQuickPick.noTasksForType", - "noProviderForTask" + "vs/platform/theme/common/tokenClassificationRegistry": [ + "schema.token.settings", + "schema.token.foreground", + "schema.token.background.warning", + "schema.token.fontStyle", + "schema.fontStyle.error", + "schema.token.fontStyle.none", + "schema.token.bold", + "schema.token.italic", + "schema.token.underline", + "schema.token.strikethrough", + "comment", + "string", + "keyword", + "number", + "regexp", + "operator", + "namespace", + "type", + "struct", + "class", + "interface", + "enum", + "typeParameter", + "function", + "member", + "method", + "macro", + "variable", + "parameter", + "property", + "enumMember", + "event", + "decorator", + "labels", + "declaration", + "documentation", + "static", + "abstract", + "deprecated", + "modification", + "async", + "readonly" ], "vs/workbench/contrib/tasks/common/taskConfiguration": [ "ConfigurationParser.invalidCWD", @@ -13006,16 +14228,38 @@ "taskTerminalStatus.infosInactive", "task.watchFirstError" ], - "vs/workbench/contrib/terminal/browser/baseTerminalBackend": [ - "ptyHostStatus", - "ptyHostStatus.short", - "nonResponsivePtyHost", - "ptyHostStatus.ariaLabel" + "vs/workbench/contrib/tasks/common/taskTemplates": [ + "dotnetCore", + "msbuild", + "externalCommand", + "Maven" + ], + "vs/workbench/contrib/tasks/browser/taskQuickPick": [ + "taskQuickPick.showAll", + "configureTaskIcon", + "removeTaskIcon", + "configureTask", + "contributedTasks", + "taskType", + "removeRecent", + "recentlyUsed", + "configured", + "configured", + "TaskQuickPick.changeSettingDetails", + "TaskQuickPick.changeSettingNo", + "TaskService.pickRunTask", + "TaskQuickPick.changeSettingsOptions", + "TaskQuickPick.goBack", + "TaskQuickPick.noTasksForType", + "noProviderForTask" ], "vs/workbench/contrib/localHistory/browser/localHistory": [ "localHistoryIcon", "localHistoryRestore" ], + "vs/workbench/contrib/multiDiffEditor/browser/icons.contribution": [ + "multiDiffEditorLabelIcon" + ], "vs/workbench/contrib/debug/common/abstractDebugAdapter": [ "timeout" ], @@ -13213,25 +14457,60 @@ "terminal.integrated.defaultProfile.osx", "terminal.integrated.defaultProfile.windows" ], + "vs/base/browser/ui/findinput/findInput": [ + "defaultLabel" + ], "vs/base/browser/ui/inputbox/inputBox": [ "alertErrorMessage", "alertWarningMessage", "alertInfoMessage", { - "key": "history.inputbox.hint", + "key": "history.inputbox.hint.suffix.noparens", + "comment": [ + "Text is the suffix of an input field placeholder coming after the action the input field performs, this will be used when the input field ends in a closing parenthesis \")\", for example \"Filter (e.g. text, !exclude)\". The character inserted into the final string is ⇅ to represent the up and down arrow keys." + ] + }, + { + "key": "history.inputbox.hint.suffix.inparens", "comment": [ - "Text will be prefixed with ⇅ plus a single space, then used as a hint where input field keeps history" + "Text is the suffix of an input field placeholder coming after the action the input field performs, this will be used when the input field does NOT end in a closing parenthesis (eg. \"Find\"). The character inserted into the final string is ⇅ to represent the up and down arrow keys." ] }, "clearedInput" ], - "vs/base/browser/ui/findinput/findInput": [ - "defaultLabel" + "vs/editor/browser/widget/diffEditor/registrations.contribution": [ + "diffEditor.move.border", + "diffEditor.moveActive.border", + "diffEditor.unchangedRegionShadow", + "diffInsertIcon", + "diffRemoveIcon" ], - "vs/editor/contrib/codeAction/browser/lightBulbWidget": [ - "preferredcodeActionWithKb", - "codeActionWithKb", - "codeAction" + "vs/editor/browser/widget/diffEditor/commands": [ + "toggleCollapseUnchangedRegions", + "toggleShowMovedCodeBlocks", + "toggleUseInlineViewWhenSpaceIsLimited", + "diffEditor", + "switchSide", + "exitCompareMove", + "collapseAllUnchangedRegions", + "showAllUnchangedRegions", + "revert", + "accessibleDiffViewer", + "editor.action.accessibleDiffViewer.next", + "editor.action.accessibleDiffViewer.prev" + ], + "vs/editor/contrib/dropOrPasteInto/browser/copyPasteController": [ + "pasteWidgetVisible", + "postPasteWidgetTitle", + "pasteAsError", + "pasteIntoEditorProgress", + "pasteAsPickerPlaceholder", + "pasteAsProgress" + ], + "vs/editor/contrib/codeAction/browser/codeActionController": [ + "editingNewSelection", + "hideMoreActions", + "showMoreActions" ], "vs/editor/contrib/codeAction/browser/codeActionCommands": [ "args.schema.kind", @@ -13263,10 +14542,11 @@ "autoFix.label", "editor.action.autoFix.noneMessage" ], - "vs/editor/contrib/codeAction/browser/codeActionController": [ - "editingNewSelection", - "hideMoreActions", - "showMoreActions" + "vs/editor/contrib/codeAction/browser/lightBulbWidget": [ + "codeActionAutoRun", + "preferredcodeActionWithKb", + "codeActionWithKb", + "codeAction" ], "vs/base/browser/ui/actionbar/actionViewItems": [ { @@ -13277,27 +14557,15 @@ ] } ], - "vs/editor/contrib/dropOrPasteInto/browser/copyPasteController": [ - "pasteWidgetVisible", - "postPasteWidgetTitle", - "pasteIntoEditorProgress", - "pasteAsPickerPlaceholder", - "pasteAsProgress" - ], "vs/editor/contrib/dropOrPasteInto/browser/defaultProviders": [ - "builtIn", "text.label", "defaultDropProvider.uriList.uris", "defaultDropProvider.uriList.uri", "defaultDropProvider.uriList.paths", "defaultDropProvider.uriList.path", "defaultDropProvider.uriList.relativePaths", - "defaultDropProvider.uriList.relativePath" - ], - "vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController": [ - "dropWidgetVisible", - "postDropWidgetTitle", - "dropIntoEditorProgress" + "defaultDropProvider.uriList.relativePath", + "pasteHtmlLabel" ], "vs/editor/contrib/find/browser/findWidget": [ "findSelectionIcon", @@ -13328,22 +14596,20 @@ "ariaSearchNoResultWithLineNumNoCurrentMatch", "ctrlEnter.keybindingChanged" ], + "vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController": [ + "dropWidgetVisible", + "postDropWidgetTitle", + "dropIntoEditorProgress" + ], "vs/editor/contrib/folding/browser/foldingDecorations": [ "foldBackgroundBackground", "editorGutter.foldingControlForeground", "foldingExpandedIcon", "foldingCollapsedIcon", "foldingManualCollapedIcon", - "foldingManualExpandedIcon" - ], - "vs/editor/contrib/format/browser/format": [ - "hint11", - "hintn1", - "hint1n", - "hintnn" - ], - "vs/editor/contrib/inlineCompletions/browser/hoverParticipant": [ - "inlineSuggestionFollows" + "foldingManualExpandedIcon", + "linesCollapsed", + "linesExpanded" ], "vs/editor/contrib/inlineCompletions/browser/commands": [ "action.inlineSuggest.showNext", @@ -13361,16 +14627,122 @@ "vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController": [ "showAccessibleViewHint" ], + "vs/editor/contrib/inlineCompletions/browser/hoverParticipant": [ + "inlineSuggestionFollows" + ], + "vs/editor/contrib/hover/browser/hoverActions": [ + { + "key": "showOrFocusHover", + "comment": [ + "Label for action that will trigger the showing/focusing of a hover in the editor.", + "If the hover is not visible, it will show the hover.", + "This allows for users to show the hover without using the mouse." + ] + }, + "showOrFocusHover.focus.noAutoFocus", + "showOrFocusHover.focus.focusIfVisible", + "showOrFocusHover.focus.autoFocusImmediately", + { + "key": "showDefinitionPreviewHover", + "comment": [ + "Label for action that will trigger the showing of definition preview hover in the editor.", + "This allows for users to show the definition preview hover without using the mouse." + ] + }, + { + "key": "scrollUpHover", + "comment": [ + "Action that allows to scroll up in the hover widget with the up arrow when the hover widget is focused." + ] + }, + { + "key": "scrollDownHover", + "comment": [ + "Action that allows to scroll down in the hover widget with the up arrow when the hover widget is focused." + ] + }, + { + "key": "scrollLeftHover", + "comment": [ + "Action that allows to scroll left in the hover widget with the left arrow when the hover widget is focused." + ] + }, + { + "key": "scrollRightHover", + "comment": [ + "Action that allows to scroll right in the hover widget with the right arrow when the hover widget is focused." + ] + }, + { + "key": "pageUpHover", + "comment": [ + "Action that allows to page up in the hover widget with the page up command when the hover widget is focused." + ] + }, + { + "key": "pageDownHover", + "comment": [ + "Action that allows to page down in the hover widget with the page down command when the hover widget is focused." + ] + }, + { + "key": "goToTopHover", + "comment": [ + "Action that allows to go to the top of the hover widget with the home command when the hover widget is focused." + ] + }, + { + "key": "goToBottomHover", + "comment": [ + "Action that allows to go to the bottom in the hover widget with the end command when the hover widget is focused." + ] + }, + { + "key": "increaseHoverVerbosityLevel", + "comment": [ + "Label for action that will increase the hover verbosity level." + ] + }, + { + "key": "decreaseHoverVerbosityLevel", + "comment": [ + "Label for action that will decrease the hover verbosity level." + ] + }, + "showOrFocusHoverDescription", + "showDefinitionPreviewHoverDescription", + "scrollUpHoverDescription", + "scrollDownHoverDescription", + "scrollLeftHoverDescription", + "scrollRightHoverDescription", + "pageUpHoverDescription", + "pageDownHoverDescription", + "goToTopHoverDescription", + "goToBottomHoverDescription" + ], + "vs/editor/contrib/hover/browser/markerHoverParticipant": [ + "view problem", + "noQuickFixes", + "checkingForQuickFixes", + "noQuickFixes", + "quick fixes" + ], + "vs/editor/contrib/hover/browser/markdownHoverParticipant": [ + "increaseHoverVerbosity", + "decreaseHoverVerbosity", + "modesContentHover.loading", + "stopped rendering", + "too many characters", + "increaseVerbosityWithKb", + "increaseVerbosity", + "decreaseVerbosityWithKb", + "decreaseVerbosity" + ], "vs/editor/contrib/gotoSymbol/browser/peek/referencesController": [ "referenceSearchVisible", "labelLoading", "metaTitle.N" ], - "vs/editor/contrib/gotoSymbol/browser/symbolNavigation": [ - "hasSymbols", - "location.kb", - "location" - ], "vs/editor/contrib/gotoSymbol/browser/referencesModel": [ "aria.oneReference", { @@ -13386,6 +14758,11 @@ "aria.result.n1", "aria.result.nm" ], + "vs/editor/contrib/gotoSymbol/browser/symbolNavigation": [ + "hasSymbols", + "location.kb", + "location" + ], "vs/editor/contrib/message/browser/messageController": [ "messageVisible" ], @@ -13415,31 +14792,6 @@ "hint.def", "hint.cmd" ], - "vs/editor/contrib/hover/browser/markdownHoverParticipant": [ - "modesContentHover.loading", - "stopped rendering", - "too many characters" - ], - "vs/editor/contrib/hover/browser/markerHoverParticipant": [ - "view problem", - "noQuickFixes", - "checkingForQuickFixes", - "noQuickFixes", - "quick fixes" - ], - "vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget": [ - "parameterHintsNextIcon", - "parameterHintsPreviousIcon", - { - "key": "content", - "comment": [ - "A label", - "A keybinding" - ] - }, - "previous", - "next" - ], "vs/editor/contrib/wordHighlighter/browser/highlightDecorations": [ "wordHighlight", "wordHighlightStrong", @@ -13457,18 +14809,21 @@ "hint", "editorHoverWidgetHighlightForeground" ], - "vs/editor/contrib/rename/browser/renameInputField": [ + "vs/editor/contrib/rename/browser/renameWidget": [ "renameInputVisible", - "renameAriaLabel", + "renameInputFocused", { "key": "label", "comment": [ "placeholders are keybindings, e.g \"F2 to Rename, Shift+F2 to Preview\"" ] - } + }, + "renameSuggestionsReceivedAria", + "renameAriaLabel", + "generateRenameSuggestionsButton", + "cancelRenameSuggestionsButton" ], "vs/editor/contrib/stickyScroll/browser/stickyScrollActions": [ - "toggleStickyScroll", { "key": "mitoggleStickyScroll", "comment": [ @@ -13482,13 +14837,15 @@ "&& denotes a mnemonic" ] }, - "focusStickyScroll", { "key": "mifocusStickyScroll", "comment": [ "&& denotes a mnemonic" ] }, + "toggleEditorStickyScroll", + "toggleEditorStickyScroll.description", + "focusStickyScroll", "selectNextStickyScrollLine.title", "selectPreviousStickyScrollLine.title", "goToFocusedStickyScrollLine.title", @@ -13512,49 +14869,25 @@ "label.desc", "ariaCurrenttSuggestionReadDetails" ], - "vs/platform/theme/common/tokenClassificationRegistry": [ - "schema.token.settings", - "schema.token.foreground", - "schema.token.background.warning", - "schema.token.fontStyle", - "schema.fontStyle.error", - "schema.token.fontStyle.none", - "schema.token.bold", - "schema.token.italic", - "schema.token.underline", - "schema.token.strikethrough", - "comment", - "string", - "keyword", - "number", - "regexp", - "operator", - "namespace", - "type", - "struct", - "class", - "interface", - "enum", - "typeParameter", - "function", - "member", - "method", - "macro", - "variable", - "parameter", - "property", - "enumMember", - "event", - "decorator", - "labels", - "declaration", - "documentation", - "static", - "abstract", - "deprecated", - "modification", - "async", - "readonly" + "vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature": [ + "foldUnchanged", + "diff.hiddenLines.top", + "showUnchangedRegion", + "diff.bottom", + "hiddenLines", + "diff.hiddenLines.expandAll" + ], + "vs/workbench/contrib/chat/browser/chatInputPart": [ + "actions.chat.accessibiltyHelp", + "chatInput.accessibilityHelpNoKb", + "chatInput", + "notebook.moreExecuteActionsLabel", + "use", + "chat.submitToSecondaryAgent" + ], + "vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables": [ + "allFiles", + "allFilesDescription" ], "vs/workbench/api/browser/mainThreadWebviews": [ "errorMessage" @@ -13577,43 +14910,15 @@ "vetoExtHostRestart", "defaultEditLabel" ], - "vs/workbench/contrib/comments/browser/commentsView": [ - "comments.filter.placeholder", - "comments.filter.ariaLabel", - "totalUnresolvedComments", - "showing filtered results", - "rootCommentsLabel", - "resourceWithCommentThreadsLabel", - "resourceWithCommentLabel", - "resourceWithCommentLabelFile", - "collapseAll", - "expandAll" - ], - "vs/workbench/contrib/comments/browser/commentsTreeViewer": [ - "comments.view.title", - "commentsCount", - "commentCount", - "imageWithLabel", - "image", - "commentLine", - "commentRange", - "lastReplyFrom" - ], "vs/workbench/contrib/testing/common/testResult": [ "runFinished" ], - "vs/workbench/browser/parts/compositeBarActions": [ - "titleKeybinding", - "badgeTitle", - "additionalViews", - "numberBadge", - "manageExtension", - "hide", - "keep", - "hideBadge", - "showBadge", - "toggle", - "toggleBadge" + "vs/base/browser/ui/tree/treeDefaults": [ + "collapse all" + ], + "vs/workbench/browser/parts/views/checkbox": [ + "checked", + "unchecked" ], "vs/base/browser/ui/splitview/paneview": [ "viewSection" @@ -13645,7 +14950,6 @@ "tunnelPrivacy.unknown", "tunnelPrivacy.private", "tunnel.focusContext", - "remote.tunnel", "tunnelView", "remote.tunnel.label", "remote.tunnelsView.labelPlaceholder", @@ -13653,13 +14957,11 @@ "remote.tunnelsView.portNumberToHigh", "remote.tunnelView.inlineElevationMessage", "remote.tunnelView.alreadyForwarded", - "remote.tunnel.forward", "remote.tunnel.forwardItem", "remote.tunnel.forwardPrompt", "remote.tunnel.forwardError", "remote.tunnel.forwardErrorProvided", "remote.tunnel.closeNoPorts", - "remote.tunnel.close", "remote.tunnel.closePlaceholder", "remote.tunnel.open", "remote.tunnel.openPreview", @@ -13678,7 +14980,10 @@ "remote.tunnel.protocolHttps", "tunnelContext.privacyMenu", "tunnelContext.protocolMenu", - "portWithRunningProcess.foreground" + "portWithRunningProcess.foreground", + "remote.tunnel", + "remote.tunnel.forward", + "remote.tunnel.close" ], "vs/workbench/contrib/remote/browser/remoteIcons": [ "getStartedIcon", @@ -13699,13 +15004,6 @@ "forwardedPortWithoutProcessIcon", "forwardedPortWithProcessIcon" ], - "vs/base/browser/ui/tree/treeDefaults": [ - "collapse all" - ], - "vs/workbench/browser/parts/views/checkbox": [ - "checked", - "unchecked" - ], "vs/workbench/browser/parts/editor/textCodeEditor": [ "textEditor" ], @@ -13714,147 +15012,68 @@ "binaryError", "openAnyway" ], - "vs/workbench/browser/parts/activitybar/activitybarActions": [ - "loading", - "authProviderUnavailable", - "manageTrustedExtensions", - "signOut", - "noAccounts", - "hideAccounts", - "manage", - "previousSideBarView", - "nextSideBarView", - "focusActivityBar" - ], - "vs/workbench/browser/parts/compositeBar": [ - "activityBarAriaLabel" - ], - "vs/workbench/browser/parts/titlebar/menubarControl": [ - { - "key": "mFile", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "mEdit", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "mSelection", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "mView", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "mGoto", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "mTerminal", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "mHelp", - "comment": [ - "&& denotes a mnemonic" - ] - }, + "vs/workbench/browser/parts/activitybar/activitybarPart": [ + "menu", + "hideMenu", + "activity bar position", { - "key": "mPreferences", + "key": "miDefaultActivityBar", "comment": [ "&& denotes a mnemonic" ] }, - "menubar.customTitlebarAccessibilityNotification", - "goToSetting", - "focusMenu", + "default", { - "key": "checkForUpdates", + "key": "miTopActivityBar", "comment": [ "&& denotes a mnemonic" ] }, - "checkingForUpdates", + "top", { - "key": "download now", + "key": "miBottomActivityBar", "comment": [ "&& denotes a mnemonic" ] }, - "DownloadingUpdate", + "bottom", { - "key": "installUpdate...", + "key": "miHideActivityBar", "comment": [ "&& denotes a mnemonic" ] }, - "installingUpdate", - { - "key": "restartToUpdate", - "comment": [ - "&& denotes a mnemonic" - ] - } + "hide", + "positionActivituBar", + "positionActivituBar", + "positionActivituBar", + "positionActivityBarDefault", + "positionActivityBarTop", + "positionActivityBarBottom", + "hideActivityBar", + "previousSideBarView", + "nextSideBarView", + "focusActivityBar" ], - "vs/workbench/browser/parts/compositePart": [ - "ariaCompositeToolbarLabel", - "viewsAndMoreActions", - "titleTooltip" + "vs/workbench/browser/parts/paneCompositePart": [ + "pane.emptyMessage", + "moreActions", + "views" ], "vs/workbench/browser/parts/sidebar/sidebarActions": [ "focusSideBar" ], - "vs/base/browser/ui/toolbar/toolbar": [ - "moreActions" - ], - "vs/workbench/browser/parts/editor/editorPanes": [ - "editorOpenErrorDialog", - { - "key": "ok", - "comment": [ - "&& denotes a mnemonic" - ] - } - ], - "vs/workbench/browser/parts/editor/editorGroupWatermark": [ - "watermark.showCommands", - "watermark.quickAccess", - "watermark.openFile", - "watermark.openFolder", - "watermark.openFileFolder", - "watermark.openRecent", - "watermark.newUntitledFile", - "watermark.findInFiles", - { - "key": "watermark.toggleTerminal", - "comment": [ - "toggle is a verb here" - ] - }, - "watermark.startDebugging", - { - "key": "watermark.toggleFullscreen", - "comment": [ - "toggle is a verb here" - ] - }, - "watermark.showSettings" + "vs/workbench/browser/parts/editor/editorGroupView": [ + "ariaLabelGroupActions", + "emptyEditorGroup", + "groupLabelLong", + "groupLabel", + "groupAriaLabelLong", + "groupAriaLabel", + "moveErrorDetails" ], - "vs/base/browser/ui/iconLabel/iconLabelHover": [ - "iconLabel.loading" + "vs/workbench/browser/parts/editor/editorDropTarget": [ + "dropIntoEditorPrompt" ], "vs/workbench/services/preferences/browser/keybindingsEditorModel": [ "default", @@ -13871,6 +15090,7 @@ "validations.stringIncorrectEnumOptions", "validations.stringIncorrectType", "invalidTypeError", + "regexParsingError", "validations.maxLength", "validations.minLength", "validations.regex", @@ -13906,9 +15126,24 @@ ] } ], - "vs/platform/quickinput/browser/quickInputController": [ - "quickInput.checkAll", - { + "vs/base/browser/ui/icons/iconSelectBox": [ + "iconSelect.placeholder", + "iconSelect.noResults" + ], + "vs/platform/quickinput/browser/quickInput": [ + "quickInput.back", + "inputModeEntry", + "quickInput.steps", + "quickInputBox.ariaLabel", + "inputModeEntryDescription" + ], + "vs/base/browser/ui/hover/hoverWidget": [ + "acessibleViewHint", + "acessibleViewHintNoKbOpen" + ], + "vs/platform/quickinput/browser/quickInputController": [ + "quickInput.checkAll", + { "key": "quickInput.visibleCount", "comment": [ "This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers." @@ -13925,10 +15160,6 @@ "quickInput.backWithKeybinding", "quickInput.back" ], - "vs/base/browser/ui/hover/hoverWidget": [ - "acessibleViewHint", - "acessibleViewHintNoKbOpen" - ], "vs/workbench/services/textMate/common/TMGrammars": [ "vscode.extension.contributes.grammars", "vscode.extension.contributes.grammars.language", @@ -13943,6 +15174,39 @@ "vs/base/browser/ui/keybindingLabel/keybindingLabel": [ "unbound" ], + "vs/workbench/contrib/preferences/browser/preferencesWidgets": [ + "userSettings", + "userSettingsRemote", + "workspaceSettings", + "folderSettings", + "settingsSwitcherBarAriaLabel", + "userSettings", + "userSettingsRemote", + "workspaceSettings", + "userSettings", + "workspaceSettings" + ], + "vs/workbench/contrib/preferences/browser/preferencesRenderers": [ + "editTtile", + "replaceDefaultValue", + "copyDefaultValue", + "unsupportedPolicySetting", + "unsupportLanguageOverrideSetting", + "defaultProfileSettingWhileNonDefaultActive", + "allProfileSettingWhileInNonDefaultProfileSetting", + "unsupportedRemoteMachineSetting", + "unsupportedWindowSetting", + "unsupportedApplicationSetting", + "unsupportedMachineSetting", + "untrustedSetting", + "unknown configuration setting", + "manage workspace trust", + "manage workspace trust", + "unsupportedProperty" + ], + "vs/base/browser/ui/toolbar/toolbar": [ + "moreActions" + ], "vs/workbench/contrib/preferences/common/settingsEditorColorRegistry": [ "headerForeground", "settingsHeaderHoverForeground", @@ -13966,36 +15230,6 @@ "settings.rowHoverBackground", "settings.focusedRowBorder" ], - "vs/workbench/contrib/preferences/browser/preferencesRenderers": [ - "editTtile", - "replaceDefaultValue", - "copyDefaultValue", - "unsupportedPolicySetting", - "unsupportLanguageOverrideSetting", - "defaultProfileSettingWhileNonDefaultActive", - "allProfileSettingWhileInNonDefaultProfileSetting", - "unsupportedRemoteMachineSetting", - "unsupportedWindowSetting", - "unsupportedApplicationSetting", - "unsupportedMachineSetting", - "untrustedSetting", - "unknown configuration setting", - "manage workspace trust", - "manage workspace trust", - "unsupportedProperty" - ], - "vs/workbench/contrib/preferences/browser/preferencesWidgets": [ - "userSettings", - "userSettingsRemote", - "workspaceSettings", - "folderSettings", - "settingsSwitcherBarAriaLabel", - "userSettings", - "userSettingsRemote", - "workspaceSettings", - "userSettings", - "workspaceSettings" - ], "vs/workbench/contrib/preferences/browser/settingsLayout": [ "commonlyUsed", "textEditor", @@ -14004,6 +15238,7 @@ "font", "formatting", "diffEditor", + "multiDiffEditor", "minimap", "suggestions", "files", @@ -14017,6 +15252,7 @@ "window", "newWindow", "features", + "accessibility.signals", "accessibility", "fileExplorer", "search", @@ -14032,7 +15268,6 @@ "remote", "timeline", "notebook", - "audioCues", "mergeEditor", "chat", "application", @@ -14046,26 +15281,6 @@ "security", "workspace" ], - "vs/workbench/contrib/preferences/browser/settingsTree": [ - "extensions", - "modified", - "settingsContextMenuTitle", - "newExtensionsButtonLabel", - "editInSettingsJson", - "editLanguageSettingLabel", - "settings.Default", - "modified", - "showExtension", - "resetSettingLabel", - "validationError", - "validationError", - "settings.Modified", - "settings", - "copySettingIdLabel", - "copySettingAsJSONLabel", - "stopSyncingSetting", - "applyToAllProfiles" - ], "vs/workbench/contrib/preferences/browser/tocTree": [ { "key": "settingsTOC", @@ -14091,9 +15306,123 @@ "policySettingsSearch", "policySettingsSearchTooltip" ], + "vs/workbench/contrib/preferences/browser/settingsTree": [ + "extensions", + "modified", + "settingsContextMenuTitle", + "newExtensionsButtonLabel", + "editInSettingsJson", + "editLanguageSettingLabel", + "settings.Default", + "modified", + "showExtension", + "resetSettingLabel", + "validationError", + "validationError", + "settings.Modified", + "settings", + "copySettingIdLabel", + "copySettingAsJSONLabel", + "stopSyncingSetting", + "applyToAllProfiles" + ], + "vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp": [ + "chat.overview", + "chat.requestHistory", + "chat.inspectResponse", + "chat.inspectResponseNoKb", + "chat.followUp", + "chat.announcement", + "workbench.action.chat.focus", + "workbench.action.chat.focusNoKb", + "workbench.action.chat.focusInput", + "workbench.action.interactiveSession.focusInputNoKb", + "workbench.action.chat.nextCodeBlock", + "workbench.action.chat.nextCodeBlockNoKb", + "workbench.action.chat.nextFileTree", + "workbench.action.chat.nextFileTreeNoKb", + "workbench.action.chat.clear", + "workbench.action.chat.clearNoKb", + "inlineChat.overview", + "inlineChat.access", + "inlineChat.requestHistory", + "inlineChat.inspectResponse", + "inlineChat.inspectResponseNoKb", + "inlineChat.contextActions", + "inlineChat.fix", + "inlineChat.diff", + "inlineChat.diffNoKb", + "inlineChat.toolbar", + "chat.signals" + ], + "vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys": [ + "inlineSuggestionVisible", + "inlineSuggestionHasIndentation", + "inlineSuggestionHasIndentationLessThanTabSize", + "suppressSuggestions" + ], + "vs/workbench/contrib/chat/browser/codeBlockPart": [ + "chat.codeBlockHelp", + "chat.codeBlock.toolbarVerbose", + "chat.codeBlock.toolbar", + "chat.codeBlockLabel", + "vulnerabilitiesPlural", + "vulnerabilitiesSingular", + "chat.codeBlockHelp", + "original", + "modified", + "chat.codeBlock.toolbarVerbose", + "chat.codeBlock.toolbar", + "chat.compareCodeBlockLabel", + "chat.edits.N", + "chat.edits.1", + "interactive.compare.apply.confirm", + "interactive.compare.apply.confirm.detail" + ], + "vs/workbench/contrib/notebook/browser/controller/cellOperations": [ + "notebookActions.joinSelectedCells", + "notebookActions.joinSelectedCells.label" + ], + "vs/workbench/contrib/chat/browser/chatAccessibilityProvider": [ + "chat", + "singleFileTreeHint", + "multiFileTreeHint", + "noCodeBlocksHint", + "noCodeBlocks", + "singleCodeBlockHint", + "singleCodeBlock", + "multiCodeBlockHint", + "multiCodeBlock" + ], + "vs/workbench/contrib/chat/browser/chatListRenderer": [ + "usedAgent", + "usingAgent", + "usedAgent", + "usingAgent", + "usedReferencesPlural", + "usedReferencesSingular", + "usedReferencesExpanded", + "usedReferencesCollapsed", + "commandButtonDisabled", + "editsSummary1", + "editsSummary", + "treeAriaLabel", + "usedReferences" + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatStrategies": [ + "change.0", + "review.1", + "change.1", + "review.N", + "change.N", + "review" + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget": [ + "inlineChatClosed" + ], "vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView": [ - "notebookActions.selectKernel", - "notebookActions.selectKernel.args" + "notebookActions.selectKernel.args", + "notebookActions.selectKernel" ], "vs/workbench/contrib/notebook/browser/notebookExtensionPoint": [ "contributes.notebook.provider", @@ -14122,12 +15451,31 @@ "contributes.preload.provider", "contributes.preload.provider.viewType", "contributes.preload.entrypoint", - "contributes.preload.localResourceRoots" + "contributes.preload.localResourceRoots", + "Notebook id", + "Notebook name", + "Notebook renderer name", + "Notebook mimetypes", + "notebooks", + "notebookRenderer" + ], + "vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView": [ + "notebook.emptyMarkdownPlaceholder", + { + "key": "notebook.error.rendererNotFound", + "comment": [ + "$0 is a placeholder for the mime type" + ] + }, + { + "key": "notebook.error.rendererFallbacksExhausted", + "comment": [ + "$0 is a placeholder for the mime type" + ] + }, + "webview title" ], "vs/workbench/contrib/notebook/browser/notebookEditorWidget": [ - "notebookTreeAriaLabelHelp", - "notebookTreeAriaLabelHelpNoKb", - "notebookTreeAriaLabel", "notebook.cellBorderColor", "notebook.focusedEditorBorder", "notebookStatusSuccessIcon.foreground", @@ -14153,22 +15501,6 @@ "notebook.cellEditorBackground", "notebook.editorBackground" ], - "vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView": [ - "notebook.emptyMarkdownPlaceholder", - { - "key": "notebook.error.rendererNotFound", - "comment": [ - "$0 is a placeholder for the mime type" - ] - }, - { - "key": "notebook.error.rendererFallbacksExhausted", - "comment": [ - "$0 is a placeholder for the mime type" - ] - }, - "webview title" - ], "vs/workbench/services/workingCopy/common/fileWorkingCopyManager": [ "fileWorkingCopyCreate.source", "fileWorkingCopyReplace.source", @@ -14177,18 +15509,22 @@ "readonly", "deleted", "confirmOverwrite", - "irreversible", + "overwriteIrreversible", { "key": "replaceButtonLabel", "comment": [ "&& denotes a mnemonic" ] + }, + "confirmMakeWriteable", + "confirmMakeWriteableDetail", + { + "key": "makeWriteableButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] } ], - "vs/platform/actions/browser/toolbar": [ - "hide", - "resetThisMenu" - ], "vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy": [ "current1", "current2", @@ -14207,87 +15543,91 @@ "kernels.detecting", "select" ], - "vs/workbench/contrib/notebook/browser/controller/cellOperations": [ - "notebookActions.joinSelectedCells", - "notebookActions.joinSelectedCells.label" + "vs/workbench/contrib/comments/common/commentContextKeys": [ + "hasCommentingRange", + "editorHasCommentingRange", + "hasCommentingProvider", + "commentThreadIsEmpty", + "commentIsEmpty", + "comment", + "commentThread", + "commentController", + "commentFocused" ], - "vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget": [ - "ariaSearchNoResultEmpty", - "ariaSearchNoResult", - "ariaSearchNoResultWithLineNumNoCurrentMatch" + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView": [ + "notebook.notebookVariables" ], - "vs/editor/contrib/codeAction/browser/codeAction": [ - "applyCodeActionFailed" + "vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext": [ + "notebookCellChatFocused", + "notebookChatHasActiveRequest", + "notebookChatUserDidEdit", + "notebookChatOuterFocusPosition" ], - "vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp": [ - "chat.overview", - "chat.requestHistory", - "chat.inspectResponse", - "chat.inspectResponseNoKb", - "chat.announcement", - "workbench.action.chat.focus", - "workbench.action.chat.focusNoKb", - "workbench.action.chat.focusInput", - "workbench.action.interactiveSession.focusInputNoKb", - "workbench.action.chat.nextCodeBlock", - "workbench.action.chat.nextCodeBlockNoKb", - "workbench.action.chat.nextFileTree", - "workbench.action.chat.nextFileTreeNoKb", - "workbench.action.chat.clear", - "workbench.action.chat.clearNoKb", - "inlineChat.overview", - "inlineChat.access", - "inlineChat.requestHistory", - "inlineChat.inspectResponse", - "inlineChat.inspectResponseNoKb", - "inlineChat.contextActions", - "inlineChat.fix", - "inlineChat.diff", - "inlineChat.diffNoKb", - "inlineChat.toolbar", - "chat.audioCues" + "vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController": [ + "default.placeholder", + "welcome.1", + "default.placeholder", + "welcome.1" ], - "vs/workbench/contrib/chat/browser/chatInputPart": [ - "actions.chat.accessibiltyHelp", - "chatInput.accessibilityHelpNoKb", - "chatInput" + "vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions": [ + "indentUsingTabs", + "indentUsingSpaces", + "changeTabDisplaySize", + "convertIndentationToSpaces", + "convertIndentationToTabs", + { + "key": "selectTabWidth", + "comment": [ + "Tab corresponds to the tab key" + ] + }, + "convertIndentation" ], - "vs/workbench/contrib/chat/browser/chatListRenderer": [ - "chat", - "commandFollowUpInfo", - "commandFollowUpInfoMany", - "singleFileTreeHint", - "multiFileTreeHint", - "noCodeBlocksHint", - "noCodeBlocks", - "singleCodeBlockHint", - "singleCodeBlock", - "multiCodeBlockHint", - "multiCodeBlock", - "chat.codeBlockHelp", - "chat.codeBlock.toolbarVerbose", - "chat.codeBlock.toolbar", - "chat.codeBlockLabel", - "treeAriaLabel" + "vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget": [ + "ariaSearchNoResultEmpty", + "ariaSearchNoResult", + "ariaSearchNoResultWithLineNumNoCurrentMatch" ], - "vs/workbench/contrib/inlineChat/browser/inlineChatStrategies": [ - "lines.0", - "lines.1", - "lines.N" + "vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions": [ + "arrowUp", + "arrowDown", + "focusChatWidget", + "focusNextChatWidget", + "apply2", + "apply3", + "discard", + "feedback.helpful", + "feedback.unhelpful", + "feedback.reportIssueForBug", + "notebookActions.menu.insertCodeCellWithChat", + "notebookActions.menu.insertCodeCellWithChat.tooltip", + "notebookActions.menu.insertCodeCellWithChat.tooltip", + "notebookActions.menu.insertCodeCellWithChat", + "notebookActions.menu.insertCodeCellWithChat.tooltip", + "notebookActions.menu.insertCode.ontoolbar", + "notebookActions.menu.insertCode.tooltip", + "focusNotebookChat", + "focusNextCell", + "focusPreviousCell", + "notebook.cell.chat.accept", + "notebook.cell.chat.stop", + "notebook.cell.chat.close", + "apply1", + "notebook.cell.chat.previousFromHistory", + "notebook.cell.chat.nextFromHistory", + "notebookActions.restoreCellprompt" ], - "vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys": [ - "inlineSuggestionVisible", - "inlineSuggestionHasIndentation", - "inlineSuggestionHasIndentationLessThanTabSize", - "suppressSuggestions" + "vs/editor/contrib/codeAction/browser/codeAction": [ + "applyCodeActionFailed" ], - "vs/workbench/contrib/inlineChat/browser/inlineChatWidget": [ - "aria-label", - "original", - "modified", - "inlineChat.accessibilityHelp", - "inlineChat.accessibilityHelpNoKb", - "inlineChatClosed" + "vs/platform/quickinput/browser/commandsQuickAccess": [ + "recentlyUsed", + "suggested", + "commonlyUsed", + "morecCommands", + "suggested", + "commandPickAriaLabelWithKeybinding", + "canNotRun" ], "vs/workbench/contrib/testing/browser/theme": [ "testing.iconFailed", @@ -14298,11 +15638,28 @@ "testing.iconUnset", "testing.iconSkipped", "testing.peekBorder", + "testing.messagePeekBorder", "testing.peekBorder", + "testing.messagePeekHeaderBackground", + "testing.coveredBackground", + "testing.coveredBorder", + "testing.coveredGutterBackground", + "testing.uncoveredBranchBackground", + "testing.uncoveredBackground", + "testing.uncoveredBorder", + "testing.uncoveredGutterBackground", + "testing.coverCountBadgeBackground", + "testing.coverCountBadgeForeground", "testing.message.error.decorationForeground", "testing.message.error.marginBackground", "testing.message.info.decorationForeground", - "testing.message.info.marginBackground" + "testing.message.info.marginBackground", + "testing.iconErrored.retired", + "testing.iconFailed.retired", + "testing.iconPassed.retired", + "testing.iconQueued.retired", + "testing.iconUnset.retired", + "testing.iconSkipped.retired" ], "vs/workbench/contrib/testing/common/constants": [ "testState.errored", @@ -14335,11 +15692,7 @@ "testing.filters.removeTestExclusions" ], "vs/workbench/contrib/terminal/browser/xterm/xtermTerminal": [ - "terminal.integrated.copySelection.noSelection", - "yes", - "no", - "dontShowAgain", - "terminal.slowRendering" + "terminal.integrated.copySelection.noSelection" ], "vs/workbench/contrib/terminal/common/terminalColorRegistry": [ "terminal.background", @@ -14364,14 +15717,32 @@ "terminal.tab.activeBorder", "terminal.ansiColor" ], - "vs/platform/quickinput/browser/commandsQuickAccess": [ - "recentlyUsed", - "suggested", - "commonlyUsed", - "morecCommands", - "suggested", - "commandPickAriaLabelWithKeybinding", - "canNotRun" + "vs/workbench/contrib/files/browser/views/explorerDecorationsProvider": [ + "canNotResolve", + "symbolicLlink", + "unknown", + "label" + ], + "vs/workbench/contrib/files/browser/views/explorerViewer": [ + "treeAriaLabel", + "fileInputAriaLabel", + "confirmRootsMove", + "confirmMultiMove", + "confirmRootMove", + "confirmMove", + "doNotAskAgain", + { + "key": "moveButtonLabel", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "copy", + "copying", + "move", + "moving", + "numberOfFolders", + "numberOfFiles" ], "vs/workbench/contrib/files/browser/fileImportExport": [ "uploadingFiles", @@ -14438,42 +15809,6 @@ ] } ], - "vs/workbench/contrib/files/browser/views/explorerViewer": [ - "treeAriaLabel", - "fileInputAriaLabel", - "confirmRootsMove", - "confirmMultiMove", - "confirmRootMove", - "confirmMove", - "doNotAskAgain", - { - "key": "moveButtonLabel", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "copy", - "copying", - "move", - "moving", - "numberOfFolders", - "numberOfFiles" - ], - "vs/workbench/contrib/files/browser/views/explorerDecorationsProvider": [ - "canNotResolve", - "symbolicLlink", - "unknown", - "label" - ], - "vs/workbench/contrib/searchEditor/browser/searchEditorSerialization": [ - "invalidQueryStringError", - "numFiles", - "oneFile", - "numResults", - "oneResult", - "noResults", - "searchMaxResultsWarning" - ], "vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree": [ "bulkEdit", "aria.renameAndEdit", @@ -14492,8 +15827,12 @@ "detail.del", "title" ], - "vs/workbench/contrib/search/browser/searchFindInput": [ - "searchFindInputNotebookFilter.label" + "vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview": [ + "default" + ], + "vs/workbench/contrib/search/browser/replaceService": [ + "searchReplace.source", + "fileReplaceChanges" ], "vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess": [ "cannotRunGotoSymbolWithoutEditor", @@ -14530,20 +15869,35 @@ "field", "constant" ], - "vs/workbench/contrib/search/browser/replaceService": [ - "searchReplace.source", - "fileReplaceChanges" + "vs/workbench/contrib/search/browser/searchFindInput": [ + "aiDescription", + "searchFindInputNotebookFilter.label" + ], + "vs/workbench/contrib/searchEditor/browser/searchEditorSerialization": [ + "invalidQueryStringError", + "numFiles", + "oneFile", + "numResults", + "oneResult", + "noResults", + "searchMaxResultsWarning" + ], + "vs/workbench/contrib/scm/browser/dirtyDiffSwitcher": [ + "remotes", + "quickDiff.base.switch" + ], + "vs/workbench/contrib/scm/browser/menus": [ + "miShare" ], "vs/workbench/contrib/debug/browser/baseDebugView": [ "debug.lazyButton.tooltip" ], - "vs/workbench/contrib/debug/browser/debugSessionPicker": [ - "moveFocusedView.selectView", - "workbench.action.debug.startDebug", - "workbench.action.debug.spawnFrom" - ], - "vs/workbench/contrib/debug/common/loadedScriptsPicker": [ - "moveFocusedView.selectView" + "vs/workbench/contrib/debug/browser/debugConfigurationManager": [ + "editLaunchConfig", + "selectConfiguration", + "DebugConfig.failed", + "workspace", + "user settings" ], "vs/workbench/contrib/debug/browser/debugAdapterManager": [ "debugNoType", @@ -14564,51 +15918,13 @@ "installExt", "selectDebug" ], - "vs/workbench/contrib/debug/browser/debugConfigurationManager": [ - "editLaunchConfig", - "selectConfiguration", - "DebugConfig.failed", - "workspace", - "user settings" - ], - "vs/workbench/contrib/debug/browser/debugTaskRunner": [ - "preLaunchTaskErrors", - "preLaunchTaskError", - "preLaunchTaskExitCode", - "preLaunchTaskTerminated", - { - "key": "debugAnyway", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "showErrors", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "abort", - "remember", - { - "key": "debugAnyway", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "rememberTask", - "invalidTaskReference", - "DebugTaskNotFoundWithTaskId", - "DebugTaskNotFound", - "taskNotTrackedWithTaskId", - "taskNotTracked" - ], "vs/workbench/contrib/debug/browser/debugSession": [ "noDebugAdapter", "noDebugAdapter", "noDebugAdapter", "noDebugAdapter", "noDebugAdapter", + "sessionDoesNotSupporBytesBreakpoints", "noDebugAdapter", "sessionNotReadyForBreakpoints", "noDebugAdapter", @@ -14641,25 +15957,72 @@ "noDebugAdapter", "noDebugAdapter", "noDebugAdapter", + "debuggingStartedNoDebug", "debuggingStarted", "debuggingStopped" ], - "vs/workbench/contrib/debug/common/debugSource": [ - "unknownSource" - ], - "vs/workbench/contrib/scm/browser/menus": [ - "miShare" - ], - "vs/workbench/contrib/scm/browser/dirtyDiffSwitcher": [ - "remotes", - "quickDiff.base.switch" - ], - "vs/base/browser/ui/findinput/replaceInput": [ - "defaultLabel", - "label.preserveCaseToggle" + "vs/workbench/contrib/debug/browser/debugTaskRunner": [ + "preLaunchTaskErrors", + "preLaunchTaskError", + "preLaunchTaskExitCode", + "preLaunchTaskTerminated", + { + "key": "debugAnyway", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "showErrors", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "abort", + "remember", + { + "key": "debugAnyway", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "rememberTask", + "invalidTaskReference", + "DebugTaskNotFoundWithTaskId", + "DebugTaskNotFound", + "taskNotTrackedWithTaskId", + "taskNotTracked" ], - "vs/base/browser/ui/dropdown/dropdownActionViewItem": [ - "moreActions" + "vs/workbench/contrib/debug/common/debugSource": [ + "unknownSource" + ], + "vs/workbench/contrib/debug/common/loadedScriptsPicker": [ + "moveFocusedView.selectView" + ], + "vs/workbench/contrib/debug/browser/debugSessionPicker": [ + "moveFocusedView.selectView", + "workbench.action.debug.startDebug", + "workbench.action.debug.spawnFrom" + ], + "vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget": [ + "parameterHintsNextIcon", + "parameterHintsPreviousIcon", + { + "key": "content", + "comment": [ + "A label", + "A keybinding" + ] + }, + "previous", + "next" + ], + "vs/workbench/contrib/markers/browser/markersTreeViewer": [ + "problemsView", + "expandedIcon", + "collapsedIcon", + "single line", + "multi line" ], "vs/workbench/contrib/markers/browser/markersTable": [ "codeColumnLabel", @@ -14667,6 +16030,39 @@ "fileColumnLabel", "sourceColumnLabel" ], + "vs/base/browser/ui/dropdown/dropdownActionViewItem": [ + "moreActions" + ], + "vs/workbench/contrib/comments/browser/commentsModel": [ + "noComments" + ], + "vs/workbench/contrib/comments/browser/commentColors": [ + "resolvedCommentIcon", + "unresolvedCommentIcon", + "commentReplyInputBackground", + "resolvedCommentBorder", + "unresolvedCommentBorder", + "commentThreadRangeBackground", + "commentThreadActiveRangeBackground" + ], + "vs/workbench/contrib/comments/browser/commentsViewActions": [ + "focusCommentsList", + "commentsClearFilterText", + "focusCommentsFilter", + "toggle unresolved", + "comments", + "unresolved", + "toggle resolved", + "comments", + "resolved" + ], + "vs/workbench/contrib/comments/browser/commentGlyphWidget": [ + "editorGutterCommentRangeForeground", + "editorOverviewRuler.commentForeground", + "editorOverviewRuler.commentUnresolvedForeground", + "editorGutterCommentGlyphForeground", + "editorGutterCommentUnresolvedGlyphForeground" + ], "vs/workbench/contrib/mergeEditor/common/mergeEditor": [ "is", "isr", @@ -14677,13 +16073,6 @@ "baseUri", "resultUri" ], - "vs/workbench/contrib/markers/browser/markersTreeViewer": [ - "problemsView", - "expandedIcon", - "collapsedIcon", - "single line", - "multi line" - ], "vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel": [ "messageN", "message1", @@ -14781,6 +16170,13 @@ "accept.first", "accept.second" ], + "vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView": [ + "result", + "mergeEditor.remainingConflicts", + "mergeEditor.remainingConflict", + "goToNextConflict", + "allConflictHandled" + ], "vs/workbench/contrib/mergeEditor/browser/view/colors": [ "mergeEditor.change.background", "mergeEditor.change.word.background", @@ -14796,24 +16192,38 @@ "mergeEditor.conflict.input1.background", "mergeEditor.conflict.input2.background" ], - "vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView": [ - "result", - "mergeEditor.remainingConflicts", - "mergeEditor.remainingConflict", - "goToNextConflict", - "allConflictHandled" - ], - "vs/workbench/contrib/comments/browser/commentsController": [ - "commentRange", - "commentRangeStart", - "hasCommentRangesKb", - "hasCommentRangesNoKb", - "hasCommentRanges", - "pickCommentService" - ], "vs/workbench/contrib/customEditor/common/contributedCustomEditors": [ "builtinProviderDisplayName" ], + "vs/platform/files/browser/htmlFileSystemProvider": [ + "fileSystemRenameError", + "fileSystemNotAllowedError" + ], + "vs/workbench/contrib/extensions/browser/extensionsViewer": [ + "error", + "Unknown Extension", + "extensions" + ], + "vs/workbench/contrib/extensions/browser/extensionFeaturesTab": [ + "activation", + "uncaught errors", + "messaages", + "last request", + "requests count session", + "requests count total", + "runtime", + "noFeatures", + "extension features list", + "revoked", + "accessExtensionFeature", + "disableAccessExtensionFeatureMessage", + "enableAccessExtensionFeatureMessage", + "revoke", + "grant", + "cancel", + "revoke", + "enable" + ], "vs/workbench/contrib/extensions/browser/extensionsWidgets": [ "ratedLabel", "sponsor", @@ -14821,8 +16231,9 @@ "syncingore.label", "activation", "startup", - "pre-release-label", "sponsor", + "workspace extension", + "local extension", "publisher verified tooltip", "updateRequired", "activation", @@ -14840,47 +16251,68 @@ "extensionPreReleaseForeground", "extensionIcon.sponsorForeground" ], - "vs/workbench/contrib/extensions/browser/extensionsViewer": [ - "error", - "Unknown Extension", - "extensions" + "vs/workbench/contrib/extensions/browser/exeBasedRecommendations": [ + "exeBasedRecommendation" ], "vs/workbench/contrib/extensions/browser/workspaceRecommendations": [ + "workspaceRecommendation", "workspaceRecommendation" ], "vs/workbench/contrib/extensions/browser/fileBasedRecommendations": [ "fileBasedRecommendation", "languageName" ], - "vs/workbench/contrib/extensions/browser/exeBasedRecommendations": [ - "exeBasedRecommendation" - ], "vs/workbench/contrib/extensions/browser/configBasedRecommendations": [ "exeBasedRecommendation" ], "vs/workbench/contrib/extensions/browser/webRecommendations": [ "reason" ], - "vs/platform/files/browser/htmlFileSystemProvider": [ - "fileSystemRenameError", - "fileSystemNotAllowedError" + "vs/platform/terminal/common/terminalLogService": [ + "terminalLoggerName" ], - "vs/workbench/contrib/terminal/browser/terminalService": [ - "terminalService.terminalCloseConfirmationSingular", - "terminalService.terminalCloseConfirmationPlural", + "vs/workbench/contrib/terminal/browser/terminalIcons": [ + "terminalViewIcon", + "renameTerminalIcon", + "killTerminalIcon", + "newTerminalIcon", + "configureTerminalProfileIcon", + "terminalDecorationMark", + "terminalDecorationIncomplete", + "terminalDecorationError", + "terminalDecorationSuccess", + "terminalCommandHistoryRemove", + "terminalCommandHistoryOutput", + "terminalCommandHistoryFuzzySearch" + ], + "vs/workbench/contrib/terminal/browser/terminalActions": [ + "showTerminalTabs", + "workbench.action.terminal.newWorkspacePlaceholder", + "terminalLaunchHelp", + "workbench.action.terminal.runActiveFile.noFile", + "noUnattachedTerminals", + "sendSequence", + "workbench.action.terminal.newWithCwd.cwd", + "workbench.action.terminal.renameWithArg.name", + "workbench.action.terminal.renameWithArg.noName", + "workbench.action.terminal.join.insufficientTerminals", + "workbench.action.terminal.join.onlySplits", + "stickyScroll", { - "key": "terminate", + "key": "miStickyScroll", "comment": [ "&& denotes a mnemonic" ] }, - "localTerminalVirtualWorkspace", - "localTerminalRemote" - ], - "vs/workbench/contrib/terminal/browser/terminalActions": [ - "showTerminalTabs", + "emptyTerminalNameInfo", + "workbench.action.terminal.newWithProfile.profileName", + "newWithProfile.location", + "newWithProfile.location.view", + "newWithProfile.location.editor", "workbench.action.terminal.newWorkspacePlaceholder", - "terminalLaunchHelp", + "workbench.action.terminal.overriddenCwdDescription", + "workbench.action.terminal.newWorkspacePlaceholder", + "workbench.action.terminal.rename.prompt", "workbench.action.terminal.newInActiveWorkspace", "workbench.action.terminal.createTerminalEditor", "workbench.action.terminal.createTerminalEditor", @@ -14889,7 +16321,10 @@ "workbench.action.terminal.focusNextPane", "workbench.action.terminal.runRecentCommand", "workbench.action.terminal.copyLastCommand", + "workbench.action.terminal.copyLastCommandOutput", + "workbench.action.terminal.copyLastCommandAndOutput", "workbench.action.terminal.goToRecentDirectory", + "goToRecentDirectory.metadata", "workbench.action.terminal.resizePaneLeft", "workbench.action.terminal.resizePaneRight", "workbench.action.terminal.resizePaneUp", @@ -14899,7 +16334,6 @@ "workbench.action.terminal.focusPrevious", "workbench.action.terminal.runSelectedText", "workbench.action.terminal.runActiveFile", - "workbench.action.terminal.runActiveFile.noFile", "workbench.action.terminal.scrollDown", "workbench.action.terminal.scrollDownPage", "workbench.action.terminal.scrollToBottom", @@ -14909,23 +16343,14 @@ "workbench.action.terminal.clearSelection", "workbench.action.terminal.detachSession", "workbench.action.terminal.attachToSession", - "noUnattachedTerminals", "quickAccessTerminal", - "workbench.action.terminal.scrollToPreviousCommand", - "workbench.action.terminal.scrollToNextCommand", "workbench.action.terminal.selectToPreviousCommand", "workbench.action.terminal.selectToNextCommand", "workbench.action.terminal.selectToPreviousLine", "workbench.action.terminal.selectToNextLine", - "sendSequence", - "workbench.action.terminal.newWithCwd.cwd", - "workbench.action.terminal.renameWithArg.name", - "workbench.action.terminal.renameWithArg.noName", "workbench.action.terminal.relaunch", "workbench.action.terminal.joinInstance", "workbench.action.terminal.join", - "workbench.action.terminal.join.insufficientTerminals", - "workbench.action.terminal.join.onlySplits", "workbench.action.terminal.splitInActiveWorkspace", "workbench.action.terminal.selectAll", "workbench.action.terminal.new", @@ -14936,78 +16361,170 @@ "workbench.action.terminal.selectDefaultShell", "workbench.action.terminal.openSettings", "workbench.action.terminal.setFixedDimensions", - "workbench.action.terminal.sizeToContentWidth", "workbench.action.terminal.clearPreviousSessionHistory", - "workbench.action.terminal.selectPrevSuggestion", - "workbench.action.terminal.selectPrevPageSuggestion", - "workbench.action.terminal.selectNextSuggestion", - "workbench.action.terminal.selectNextPageSuggestion", - "workbench.action.terminal.acceptSelectedSuggestion", - "workbench.action.terminal.hideSuggestWidget", + "workbench.action.terminal.toggleStickyScroll", "workbench.action.terminal.copySelection", "workbench.action.terminal.copyAndClearSelection", "workbench.action.terminal.copySelectionAsHtml", "workbench.action.terminal.paste", "workbench.action.terminal.pasteSelection", "workbench.action.terminal.switchTerminal", - "emptyTerminalNameInfo", - "workbench.action.terminal.newWithProfile", - "workbench.action.terminal.newWithProfile.profileName", - "workbench.action.terminal.newWorkspacePlaceholder", - "workbench.action.terminal.overriddenCwdDescription", - "workbench.action.terminal.newWorkspacePlaceholder", - "workbench.action.terminal.rename.prompt" - ], - "vs/workbench/contrib/terminal/browser/terminalQuickAccess": [ - "workbench.action.terminal.newplus", - "workbench.action.terminal.newWithProfilePlus", - "renameTerminal" + "workbench.action.terminal.newWithProfile" ], - "vs/workbench/contrib/terminal/common/terminalConfiguration": [ - "cwd", - "cwdFolder", - "workspaceFolder", - "local", - "process", - "separator", - "sequence", - "task", - "terminalTitle", - "terminalDescription", - "terminalIntegratedConfigurationTitle", - "terminal.integrated.sendKeybindingsToShell", - "terminal.integrated.tabs.defaultColor", - "terminal.integrated.tabs.defaultIcon", - "terminal.integrated.tabs.enabled", - "terminal.integrated.tabs.enableAnimation", - "terminal.integrated.tabs.hideCondition", - "terminal.integrated.tabs.hideCondition.never", - "terminal.integrated.tabs.hideCondition.singleTerminal", - "terminal.integrated.tabs.hideCondition.singleGroup", - "terminal.integrated.tabs.showActiveTerminal", - "terminal.integrated.tabs.showActiveTerminal.always", - "terminal.integrated.tabs.showActiveTerminal.singleTerminal", - "terminal.integrated.tabs.showActiveTerminal.singleTerminalOrNarrow", - "terminal.integrated.tabs.showActiveTerminal.never", - "terminal.integrated.tabs.showActions", - "terminal.integrated.tabs.showActions.always", - "terminal.integrated.tabs.showActions.singleTerminal", - "terminal.integrated.tabs.showActions.singleTerminalOrNarrow", - "terminal.integrated.tabs.showActions.never", - "terminal.integrated.tabs.location.left", - "terminal.integrated.tabs.location.right", - "terminal.integrated.tabs.location", - "terminal.integrated.defaultLocation.editor", - "terminal.integrated.defaultLocation.view", - "terminal.integrated.defaultLocation", - "terminal.integrated.tabs.focusMode.singleClick", - "terminal.integrated.tabs.focusMode.doubleClick", - "terminal.integrated.tabs.focusMode", + "vs/workbench/contrib/terminal/browser/terminalMenus": [ + { + "key": "miNewTerminal", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miSplitTerminal", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miRunActiveFile", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "miRunSelectedText", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "workbench.action.terminal.copySelection.short", + "workbench.action.terminal.copySelectionAsHtml", + "workbench.action.terminal.paste.short", + "workbench.action.terminal.clear", + "workbench.action.terminal.selectAll", + "workbench.action.terminal.copySelection.short", + "workbench.action.terminal.copySelectionAsHtml", + "workbench.action.terminal.paste.short", + "workbench.action.terminal.clear", + "workbench.action.terminal.selectAll", + "workbench.action.terminal.newWithProfile.short", + "workbench.action.terminal.openSettings", + "workbench.action.tasks.runTask", + "workbench.action.tasks.configureTaskRunner", + "workbench.action.terminal.clearLong", + "workbench.action.terminal.runActiveFile", + "workbench.action.terminal.runSelectedText", + "workbench.action.terminal.renameInstance", + "workbench.action.terminal.changeIcon", + "workbench.action.terminal.changeColor", + "workbench.action.terminal.joinInstance", + "defaultTerminalProfile", + "defaultTerminalProfile", + "defaultTerminalProfile", + "splitTerminal", + "launchProfile", + "workbench.action.terminal.selectDefaultProfile", + "workbench.action.terminal.switchTerminal" + ], + "vs/workbench/contrib/terminal/browser/terminalWslRecommendationContribution": [ + "useWslExtension.title", + "install" + ], + "vs/workbench/contrib/terminal/browser/terminalQuickAccess": [ + "workbench.action.terminal.newplus", + "workbench.action.terminal.newWithProfilePlus", + "renameTerminal" + ], + "vs/workbench/contrib/terminal/browser/terminalService": [ + "terminalService.terminalCloseConfirmationSingular", + "terminalService.terminalCloseConfirmationPlural", + { + "key": "terminate", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "localTerminalVirtualWorkspace", + "localTerminalRemote" + ], + "vs/workbench/contrib/terminal/common/terminalStrings": [ + "terminal", + "terminal.new", + "doNotShowAgain", + "currentSessionCategory", + "previousSessionCategory", + "task", + "local", + "killTerminal.short", + "splitTerminal.short", + "terminalCategory", + "workbench.action.terminal.focus", + "workbench.action.terminal.focusAndHideAccessibleBuffer", + "killTerminal", + "moveToEditor", + "moveIntoNewWindow", + "workbench.action.terminal.moveToTerminalPanel", + "workbench.action.terminal.changeIcon", + "workbench.action.terminal.changeColor", + "splitTerminal", + "unsplitTerminal", + "workbench.action.terminal.rename", + "workbench.action.terminal.sizeToContentWidthInstance", + "workbench.action.terminal.focusHover", + "workbench.action.terminal.sendSequence", + "workbench.action.terminal.newWithCwd", + "workbench.action.terminal.renameWithArg", + "stickyScroll", + "workbench.action.terminal.scrollToPreviousCommand", + "workbench.action.terminal.scrollToNextCommand" + ], + "vs/workbench/contrib/terminal/common/terminalConfiguration": [ + "cwd", + "cwdFolder", + "workspaceFolder", + "local", + "process", + "separator", + "sequence", + "task", + "terminalTitle", + "terminalDescription", + "terminalIntegratedConfigurationTitle", + "terminal.integrated.sendKeybindingsToShell", + "terminal.integrated.tabs.defaultColor", + "terminal.integrated.tabs.defaultIcon", + "terminal.integrated.tabs.enabled", + "terminal.integrated.tabs.enableAnimation", + "terminal.integrated.tabs.hideCondition", + "terminal.integrated.tabs.hideCondition.never", + "terminal.integrated.tabs.hideCondition.singleTerminal", + "terminal.integrated.tabs.hideCondition.singleGroup", + "terminal.integrated.tabs.showActiveTerminal", + "terminal.integrated.tabs.showActiveTerminal.always", + "terminal.integrated.tabs.showActiveTerminal.singleTerminal", + "terminal.integrated.tabs.showActiveTerminal.singleTerminalOrNarrow", + "terminal.integrated.tabs.showActiveTerminal.never", + "terminal.integrated.tabs.showActions", + "terminal.integrated.tabs.showActions.always", + "terminal.integrated.tabs.showActions.singleTerminal", + "terminal.integrated.tabs.showActions.singleTerminalOrNarrow", + "terminal.integrated.tabs.showActions.never", + "terminal.integrated.tabs.location.left", + "terminal.integrated.tabs.location.right", + "terminal.integrated.tabs.location", + "terminal.integrated.defaultLocation.editor", + "terminal.integrated.defaultLocation.view", + "terminal.integrated.defaultLocation", + "terminal.integrated.tabs.focusMode.singleClick", + "terminal.integrated.tabs.focusMode.doubleClick", + "terminal.integrated.tabs.focusMode", "terminal.integrated.macOptionIsMeta", "terminal.integrated.macOptionClickForcesSelection", "terminal.integrated.altClickMovesCursor", "terminal.integrated.copyOnSelection", "terminal.integrated.enableMultiLinePasteWarning", + "terminal.integrated.enableMultiLinePasteWarning.auto", + "terminal.integrated.enableMultiLinePasteWarning.always", + "terminal.integrated.enableMultiLinePasteWarning.never", "terminal.integrated.drawBoldTextInBrightColors", "terminal.integrated.fontFamily", "terminal.integrated.fontSize", @@ -15043,6 +16560,9 @@ "terminal.integrated.rightClickBehavior.selectWord", "terminal.integrated.rightClickBehavior.nothing", "terminal.integrated.rightClickBehavior", + "terminal.integrated.middleClickBehavior.default", + "terminal.integrated.middleClickBehavior.paste", + "terminal.integrated.middleClickBehavior", "terminal.integrated.cwd", "terminal.integrated.confirmOnExit", "terminal.integrated.confirmOnExit.never", @@ -15054,6 +16574,7 @@ "terminal.integrated.confirmOnKill.panel", "terminal.integrated.confirmOnKill.always", "terminal.integrated.enableBell", + "terminal.integrated.enableVisualBell", "terminal.integrated.commandsToSkipShell", "openDefaultSettingsJson", "openDefaultSettingsJson.capitalized", @@ -15078,6 +16599,7 @@ "enableFileLinks.off", "enableFileLinks.on", "enableFileLinks.notRemote", + "terminal.integrated.allowedLinkSchemes", "terminal.integrated.unicodeVersion.six", "terminal.integrated.unicodeVersion.eleven", "terminal.integrated.unicodeVersion", @@ -15098,6 +16620,7 @@ "hideOnStartup.whenEmpty", "hideOnStartup.always", "terminal.integrated.customGlyphs", + "terminal.integrated.rescaleOverlappingGlyphs", "terminal.integrated.autoReplies", "terminal.integrated.autoReplies.reply", "terminal.integrated.shellIntegration.enabled", @@ -15108,109 +16631,20 @@ "terminal.integrated.shellIntegration.decorationsEnabled.never", "terminal.integrated.shellIntegration.history", "terminal.integrated.shellIntegration.suggestEnabled", + "suggestEnabled.deprecated", "terminal.integrated.smoothScrolling", "terminal.integrated.ignoreBracketedPasteMode", "terminal.integrated.enableImages", "terminal.integrated.focusAfterRun", "terminal.integrated.focusAfterRun.terminal", "terminal.integrated.focusAfterRun.accessible-buffer", - "terminal.integrated.focusAfterRun.none" - ], - "vs/workbench/contrib/terminal/browser/terminalMenus": [ - { - "key": "miNewTerminal", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "miSplitTerminal", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "miRunActiveFile", - "comment": [ - "&& denotes a mnemonic" - ] - }, - { - "key": "miRunSelectedText", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "workbench.action.terminal.copySelection.short", - "workbench.action.terminal.copySelectionAsHtml", - "workbench.action.terminal.paste.short", - "workbench.action.terminal.clear", - "workbench.action.terminal.selectAll", - "workbench.action.terminal.copySelection.short", - "workbench.action.terminal.copySelectionAsHtml", - "workbench.action.terminal.paste.short", - "workbench.action.terminal.clear", - "workbench.action.terminal.selectAll", - "workbench.action.terminal.newWithProfile.short", - "workbench.action.terminal.selectDefaultProfile", - "workbench.action.terminal.openSettings", - "workbench.action.tasks.runTask", - "workbench.action.tasks.configureTaskRunner", - "workbench.action.terminal.switchTerminal", - "workbench.action.terminal.clearLong", - "workbench.action.terminal.runActiveFile", - "workbench.action.terminal.runSelectedText", - "workbench.action.terminal.renameInstance", - "workbench.action.terminal.changeIcon", - "workbench.action.terminal.changeColor", - "workbench.action.terminal.sizeToContentWidthInstance", - "workbench.action.terminal.joinInstance", - "defaultTerminalProfile", - "defaultTerminalProfile", - "defaultTerminalProfile", - "splitTerminal" - ], - "vs/workbench/contrib/terminal/browser/terminalIcons": [ - "terminalViewIcon", - "renameTerminalIcon", - "killTerminalIcon", - "newTerminalIcon", - "configureTerminalProfileIcon", - "terminalDecorationMark", - "terminalDecorationIncomplete", - "terminalDecorationError", - "terminalDecorationSuccess", - "terminalCommandHistoryRemove", - "terminalCommandHistoryOutput", - "terminalCommandHistoryFuzzySearch" - ], - "vs/workbench/contrib/terminal/common/terminalStrings": [ - "terminal", - "terminal.new", - "doNotShowAgain", - "currentSessionCategory", - "previousSessionCategory", - "terminalCategory", - "workbench.action.terminal.focus", - "workbench.action.terminal.focusAndHideAccessibleBuffer", - "killTerminal", - "killTerminal.short", - "moveToEditor", - "workbench.action.terminal.moveToTerminalPanel", - "workbench.action.terminal.changeIcon", - "workbench.action.terminal.changeColor", - "splitTerminal", - "splitTerminal.short", - "unsplitTerminal", - "workbench.action.terminal.rename", - "workbench.action.terminal.sizeToContentWidthInstance", - "workbench.action.terminal.focusHover", - "workbench.action.terminal.sendSequence", - "workbench.action.terminal.newWithCwd", - "workbench.action.terminal.renameWithArg" - ], - "vs/platform/terminal/common/terminalLogService": [ - "terminalLoggerName" + "terminal.integrated.focusAfterRun.none", + "terminal.integrated.accessibleViewPreserveCursorPosition", + "terminal.integrated.accessibleViewFocusOnCommandExecution", + "terminal.integrated.stickyScroll.enabled", + "terminal.integrated.stickyScroll.maxLineCount", + "terminal.integrated.mouseWheelZoom.mac", + "terminal.integrated.mouseWheelZoom" ], "vs/workbench/contrib/terminal/browser/terminalTabbedView": [ "moveTabsRight", @@ -15230,8 +16664,10 @@ "shellProcessTooltip.commandLine" ], "vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp": [ - "focusAccessibleBuffer", - "focusAccessibleBufferNoKb", + "focusAccessibleTerminalView", + "focusAccessibleTerminalViewNoKb", + "preserveCursor", + "focusViewOnExecution", "commandPromptMigration", "shellIntegration", "goToNextCommand", @@ -15250,8 +16686,18 @@ "openDetectedLinkNoKb", "newWithProfile", "newWithProfileNoKb", - "focusAfterRun", - "accessibilitySettings" + "focusAfterRun" + ], + "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager": [ + "scheme", + "allow", + "terminalLinkHandler.followLinkAlt.mac", + "terminalLinkHandler.followLinkAlt", + "terminalLinkHandler.followLinkCmd", + "terminalLinkHandler.followLinkCtrl", + "followLink", + "followForwardedLink", + "followLinkUrl" ], "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick": [ "terminal.integrated.urlLinks", @@ -15264,19 +16710,23 @@ "terminal.integrated.localFolderLinks", "terminal.integrated.searchLinks" ], - "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager": [ - "terminalLinkHandler.followLinkAlt.mac", - "terminalLinkHandler.followLinkAlt", - "terminalLinkHandler.followLinkCmd", - "terminalLinkHandler.followLinkCtrl", - "followLink", - "followForwardedLink", - "followLinkUrl" - ], - "vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon": [ - "quickFix.command", - "quickFix.opener", - "codeAction.widget.id.quickfix" + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp": [ + "inlineChat.overview", + "inlineChat.access", + "inlineChat.input", + "inlineChat.inputNoKb", + "inlineChat.inspectResponseMessage", + "inlineChat.inspectResponseNoKb", + "inlineChat.focusResponse", + "inlineChat.focusResponseNoKb", + "inlineChat.focusInput", + "inlineChat.focusInputNoKb", + "inlineChat.runCommand", + "inlineChat.runCommandNoKb", + "inlineChat.insertCommand", + "inlineChat.insertCommandNoKb", + "inlineChat.toolbar", + "chat.signals" ], "vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixBuiltinActions": [ "terminal.freePort", @@ -15290,6 +16740,35 @@ "vscode.extension.contributes.terminalQuickFixes.commandExitResult", "vscode.extension.contributes.terminalQuickFixes.kind" ], + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions": [ + "startChat", + "closeChat", + "focusTerminalResponse", + "focusTerminalInput", + "discard", + "discardDescription", + "runCommand", + "run", + "runFirstCommand", + "runFirst", + "insertCommand", + "insert", + "insertFirstCommand", + "insertFirst", + "viewInChat", + "makeChatRequest", + "cancelChat", + "reportIssue" + ], + "vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon": [ + "quickFix.command", + "quickFix.opener", + "codeAction.widget.id.quickfix" + ], + "vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry": [ + "terminalStickyScroll.background", + "terminalStickyScrollHover.background" + ], "vs/workbench/contrib/tasks/common/jsonSchemaCommon": [ "JsonSchema.options", "JsonSchema.options.cwd", @@ -15355,8 +16834,11 @@ "JsonSchema.input.command.args" ], "vs/workbench/contrib/remote/browser/explorerViewItems": [ - "remotes", - "remote.explorer.switch" + "switchRemote.label" + ], + "vs/base/browser/ui/findinput/replaceInput": [ + "defaultLabel", + "label.preserveCaseToggle" ], "vs/workbench/contrib/snippets/browser/commands/abstractSnippetsActions": [ "snippets" @@ -15380,10 +16862,14 @@ "snippetSuggest.longLabel", "snippetSuggest.longLabel" ], + "vs/workbench/contrib/update/browser/releaseNotesEditor": [ + "releaseNotesInputName", + "unassigned", + "showOnUpdate" + ], "vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent": [ "getting-started-setup-icon", "getting-started-beginner-icon", - "getting-started-intermediate-icon", "gettingStarted.newFile.title", "gettingStarted.newFile.description", "gettingStarted.openMac.title", @@ -15409,18 +16895,21 @@ "gettingStarted.pickColor.title", "gettingStarted.pickColor.description.interpolated", "titleID", + "gettingStarted.extensions.title", + "gettingStarted.extensionsWeb.description.interpolated", + "browsePopularWeb", + "gettingStarted.findLanguageExts.title", + "gettingStarted.findLanguageExts.description.interpolated", + "browseLangExts", + "gettingStarted.settings.title", + "gettingStarted.settings.description.interpolated", + "tweakSettings", "gettingStarted.settingsSync.title", "gettingStarted.settingsSync.description.interpolated", "enableSync", "gettingStarted.commandPalette.title", "gettingStarted.commandPalette.description.interpolated", "commandPalette", - "gettingStarted.extensions.title", - "gettingStarted.extensionsWeb.description.interpolated", - "browsePopular", - "gettingStarted.findLanguageExts.title", - "gettingStarted.findLanguageExts.description.interpolated", - "browseLangExts", "gettingStarted.setup.OpenFolder.title", "gettingStarted.setup.OpenFolder.description.interpolated", "pickFolder", @@ -15430,26 +16919,29 @@ "gettingStarted.quickOpen.title", "gettingStarted.quickOpen.description.interpolated", "quickOpen", + "gettingStarted.videoTutorial.title", + "gettingStarted.videoTutorial.description.interpolated", + "watch", "gettingStarted.setupWeb.title", "gettingStarted.setupWeb.description", "gettingStarted.pickColor.title", "gettingStarted.pickColor.description.interpolated", "titleID", - "gettingStarted.settingsSync.title", - "gettingStarted.settingsSync.description.interpolated", - "enableSync", - "gettingStarted.commandPalette.title", - "gettingStarted.commandPalette.description.interpolated", - "commandPalette", "gettingStarted.menuBar.title", "gettingStarted.menuBar.description.interpolated", "toggleMenuBar", "gettingStarted.extensions.title", "gettingStarted.extensionsWeb.description.interpolated", - "browsePopular", + "browsePopularWeb", "gettingStarted.findLanguageExts.title", "gettingStarted.findLanguageExts.description.interpolated", "browseLangExts", + "gettingStarted.settingsSync.title", + "gettingStarted.settingsSync.description.interpolated", + "enableSync", + "gettingStarted.commandPalette.title", + "gettingStarted.commandPalette.description.interpolated", + "commandPalette", "gettingStarted.setup.OpenFolder.title", "gettingStarted.setup.OpenFolderWeb.description.interpolated", "openFolder", @@ -15459,33 +16951,12 @@ "quickOpen", "gettingStarted.beginner.title", "gettingStarted.beginner.description", - "gettingStarted.playground.title", - "gettingStarted.playground.description.interpolated", - "openEditorPlayground", + "gettingStarted.extensions.title", + "gettingStarted.extensions.description.interpolated", + "browsePopular", "gettingStarted.terminal.title", "gettingStarted.terminal.description.interpolated", "showTerminal", - "gettingStarted.extensions.title", - "gettingStarted.extensions.description.interpolated", - "browseRecommended", - "gettingStarted.settings.title", - "gettingStarted.settings.description.interpolated", - "tweakSettings", - "gettingStarted.profiles.title", - "gettingStarted.profiles.description.interpolated", - "tryProfiles", - "gettingStarted.workspaceTrust.title", - "gettingStarted.workspaceTrust.description.interpolated", - "workspaceTrust", - "enableTrust", - "gettingStarted.videoTutorial.title", - "gettingStarted.videoTutorial.description.interpolated", - "watch", - "gettingStarted.intermediate.title", - "gettingStarted.intermediate.description", - "gettingStarted.splitview.title", - "gettingStarted.splitview.description.interpolated", - "splitEditor", "gettingStarted.debug.title", "gettingStarted.debug.description.interpolated", "runProject", @@ -15512,15 +16983,14 @@ "gettingStarted.shortcuts.title", "gettingStarted.shortcuts.description.interpolated", "keyboardShortcuts", + "gettingStarted.workspaceTrust.title", + "gettingStarted.workspaceTrust.description.interpolated", + "workspaceTrust", + "enableTrust", "gettingStarted.notebook.title", "gettingStarted.notebookProfile.title", "gettingStarted.notebookProfile.description" ], - "vs/workbench/contrib/update/browser/releaseNotesEditor": [ - "releaseNotesInputName", - "unassigned", - "showOnUpdate" - ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedExtensionPoint": [ "title", "walkthroughs", @@ -15560,12 +17030,6 @@ "walkthroughs.steps.oneOn.command", "walkthroughs.steps.when" ], - "vs/workbench/contrib/welcomeWalkthrough/common/walkThroughUtils": [ - "walkThrough.embeddedEditorBackground" - ], - "vs/workbench/contrib/welcomeGettingStarted/browser/featuredExtensionService": [ - "gettingStarted.featuredTitle" - ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors": [ "welcomePage.background", "welcomePage.tileBackground", @@ -15575,6 +17039,19 @@ "welcomePage.progress.foreground", "walkthrough.stepTitle.foreground" ], + "vs/workbench/contrib/welcomeWalkthrough/common/walkThroughUtils": [ + "walkThrough.embeddedEditorBackground" + ], + "vs/workbench/contrib/callHierarchy/browser/callHierarchyTree": [ + "tree.aria", + "from", + "to" + ], + "vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree": [ + "tree.aria", + "supertypes", + "subtypes" + ], "vs/editor/contrib/symbolIcons/browser/symbolIcons": [ "symbolIcon.arrayForeground", "symbolIcon.booleanForeground", @@ -15610,24 +17087,9 @@ "symbolIcon.unitForeground", "symbolIcon.variableForeground" ], - "vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree": [ - "tree.aria", - "supertypes", - "subtypes" - ], - "vs/workbench/contrib/callHierarchy/browser/callHierarchyTree": [ - "tree.aria", - "from", - "to" - ], "vs/workbench/contrib/userDataSync/browser/userDataSyncViews": [ - "conflicts", - "synced machines", "workbench.actions.sync.editMachineName", "workbench.actions.sync.turnOffSyncOnMachine", - "remote sync activity title", - "local sync activity title", - "downloaded sync activity title", "workbench.actions.sync.loadActivity", "select sync activity file", "workbench.actions.sync.resolveResourceRef", @@ -15652,7 +17114,6 @@ "A confirmation message to replace current user data (settings, extensions, keybindings, snippets) with selected version" ] }, - "troubleshoot", "reset", "sideBySideLabels", { @@ -15693,16 +17154,13 @@ "comment": [ "Represents current log file" ] - } - ], - "vs/workbench/browser/parts/notifications/notificationsList": [ - "notificationAccessibleViewHint", - "notificationAccessibleViewHintNoKb", - "notificationAriaLabelHint", - "notificationAriaLabel", - "notificationWithSourceAriaLabelHint", - "notificationWithSourceAriaLabel", - "notificationsList" + }, + "conflicts", + "synced machines", + "remote sync activity title", + "local sync activity title", + "downloaded sync activity title", + "troubleshoot" ], "vs/workbench/browser/parts/notifications/notificationsActions": [ "clearIcon", @@ -15715,15 +17173,106 @@ "clearNotification", "clearNotifications", "toggleDoNotDisturbMode", + "toggleDoNotDisturbModeBySource", + "configureDoNotDisturbMode", "hideNotificationsCenter", "expandNotification", "collapseNotification", "configureNotification", "copyNotification" ], + "vs/workbench/browser/parts/notifications/notificationsList": [ + "notificationAccessibleViewHint", + "notificationAccessibleViewHintNoKb", + "notificationAriaLabelHint", + "notificationAriaLabel", + "notificationWithSourceAriaLabelHint", + "notificationWithSourceAriaLabel", + "notificationsList" + ], "vs/workbench/services/textfile/common/textFileSaveParticipant": [ "saveParticipants" ], + "vs/workbench/browser/parts/titlebar/menubarControl": [ + { + "key": "mFile", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "mEdit", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "mSelection", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "mView", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "mGoto", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "mTerminal", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "mHelp", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "mPreferences", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "menubar.customTitlebarAccessibilityNotification", + "goToSetting", + { + "key": "checkForUpdates", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "checkingForUpdates", + { + "key": "download now", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "DownloadingUpdate", + { + "key": "installUpdate...", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "installingUpdate", + { + "key": "restartToUpdate", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "focusMenu" + ], "vs/workbench/browser/parts/titlebar/commandCenterControl": [ "label.dfl", "label1", @@ -15732,10 +17281,43 @@ "title2", "title3" ], - "vs/workbench/browser/parts/titlebar/windowTitle": [ - "userIsAdmin", - "userIsSudo", - "devExtensionWindowTitlePrefix" + "vs/workbench/browser/parts/editor/editorTabsControl": [ + "ariaLabelEditorActions", + "draggedEditorGroup" + ], + "vs/workbench/browser/parts/globalCompositeBar": [ + "accountsViewBarIcon", + "hideAccounts", + "manage", + "accounts", + "accounts", + "loading", + "authProviderUnavailable", + "manageTrustedExtensions", + "signOut", + "noAccounts", + "manage", + "manage profile", + "hideAccounts", + "accounts", + "manage" + ], + "vs/workbench/browser/parts/titlebar/titlebarActions": [ + "toggle.commandCenter", + "toggle.commandCenterDescription", + "toggle.layout", + "toggle.layoutDescription", + "toggle.hideCustomTitleBar", + "toggle.hideCustomTitleBarInFullScreen", + "toggle.customTitleBar", + "toggle.editorActions", + "accounts", + "accounts", + "manage", + "manage", + "showCustomTitleBar", + "hideCustomTitleBar", + "hideCustomTitleBarInFullScreen" ], "vs/workbench/services/workingCopy/common/storedFileWorkingCopy": [ "staleSaveError", @@ -15772,6 +17354,22 @@ "vs/workbench/contrib/webview/browser/webviewElement": [ "fatalErrorMessage" ], + "vs/editor/browser/widget/multiDiffEditor/colors": [ + "multiDiffEditor.headerBackground", + "multiDiffEditor.background", + "multiDiffEditor.border" + ], + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChat": [ + "chatFocusedContextKey", + "chatVisibleContextKey", + "chatRequestActiveContextKey", + "chatInputHasTextContextKey", + "chatAgentRegisteredContextKey", + "chatResponseContainsCodeBlockContextKey", + "chatResponseContainsMultipleCodeBlocksContextKey", + "chatResponseSupportsIssueReportingContextKey", + "interactiveSessionResponseVote" + ], "vs/workbench/api/common/extHostDiagnostics": [ { "key": "limitHit", @@ -15780,13 +17378,14 @@ ] } ], + "vs/workbench/api/common/extHostNotebook": [ + "err.readonly", + "fileModifiedError" + ], "vs/workbench/api/common/extHostLanguageFeatures": [ "defaultPasteLabel", "defaultDropLabel" ], - "vs/workbench/api/common/extHostProgress": [ - "extensionSource" - ], "vs/workbench/api/common/extHostStatusBar": [ "extensionLabel", "status.extensionMessage" @@ -15794,71 +17393,11 @@ "vs/workbench/api/common/extHostTreeViews": [ "treeView.duplicateElement" ], - "vs/workbench/api/common/extHostNotebook": [ - "err.readonly", - "fileModifiedError" - ], - "vs/workbench/api/common/extHostChat": [ - "emptyResponse", - "errorResponse" - ], "vs/base/browser/ui/findinput/findInputToggles": [ "caseDescription", "wordsDescription", "regexDescription" ], - "vs/editor/browser/widget/diffEditor/accessibleDiffViewer": [ - "accessibleDiffViewerInsertIcon", - "accessibleDiffViewerRemoveIcon", - "accessibleDiffViewerCloseIcon", - "label.close", - "ariaLabel", - "no_lines_changed", - "one_line_changed", - "more_lines_changed", - { - "key": "header", - "comment": [ - "This is the ARIA label for a git diff header.", - "A git diff header looks like this: @@ -154,12 +159,39 @@.", - "That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.", - "Variables 0 and 1 refer to the diff index out of total number of diffs.", - "Variables 2 and 4 will be numbers (a line number).", - "Variables 3 and 5 will be \"no lines changed\", \"1 line changed\" or \"X lines changed\", localized separately." - ] - }, - "blankLine", - { - "key": "unchangedLine", - "comment": [ - "The placeholders are contents of the line and should not be translated." - ] - }, - "equalLine", - "insertLine", - "deleteLine" - ], - "vs/editor/browser/widget/diffEditor/movedBlocksLines": [ - "codeMovedToWithChanges", - "codeMovedFromWithChanges", - "codeMovedTo", - "codeMovedFrom" - ], - "vs/editor/browser/widget/diffEditor/hideUnchangedRegionsFeature": [ - "foldUnchanged", - "diff.hiddenLines.top", - "showAll", - "diff.bottom", - "hiddenLines", - "diff.hiddenLines.expandAll" - ], - "vs/editor/browser/widget/diffEditor/diffEditorEditors": [ - "diff-aria-navigation-tip" - ], - "vs/editor/browser/widget/diffEditor/colors": [ - "diffEditor.move.border", - "diffEditor.moveActive.border" - ], "vs/editor/browser/controller/textAreaHandler": [ "editor", "accessibilityModeOff", @@ -15866,15 +17405,6 @@ "accessibilityOffAriaLabelNoKb", "accessibilityOffAriaLabelNoKbs" ], - "vs/platform/actionWidget/browser/actionWidget": [ - "actionBar.toggledBackground", - "codeActionMenuVisible", - "hideCodeActionWidget.title", - "selectPrevCodeAction.title", - "selectNextCodeAction.title", - "acceptSelected.title", - "previewSelected.title" - ], "vs/editor/contrib/codeAction/browser/codeActionMenu": [ "codeAction.widget.id.more", "codeAction.widget.id.quickfix", @@ -15885,6 +17415,15 @@ "codeAction.widget.id.surround", "codeAction.widget.id.source" ], + "vs/platform/actionWidget/browser/actionWidget": [ + "actionBar.toggledBackground", + "codeActionMenuVisible", + "hideCodeActionWidget.title", + "selectPrevCodeAction.title", + "selectNextCodeAction.title", + "acceptSelected.title", + "previewSelected.title" + ], "vs/editor/contrib/colorPicker/browser/colorPickerWidget": [ "clickToToggleColorOptions", "closeIcon" @@ -15954,19 +17493,52 @@ "suggestMoreInfoIcon", "readMore" ], - "vs/workbench/contrib/comments/common/commentModel": [ - "noComments" + "vs/workbench/contrib/chat/browser/chatFollowups": [ + "followUpAriaLabel" ], - "vs/workbench/contrib/comments/browser/commentsViewActions": [ - "focusCommentsList", - "commentsClearFilterText", - "focusCommentsFilter", - "toggle unresolved", - "comments", - "unresolved", - "toggle resolved", - "comments", - "resolved" + "vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer": [ + "accessibleDiffViewerInsertIcon", + "accessibleDiffViewerRemoveIcon", + "accessibleDiffViewerCloseIcon", + "label.close", + "ariaLabel", + "no_lines_changed", + "one_line_changed", + "more_lines_changed", + { + "key": "header", + "comment": [ + "This is the ARIA label for a git diff header.", + "A git diff header looks like this: @@ -154,12 +159,39 @@.", + "That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.", + "Variables 0 and 1 refer to the diff index out of total number of diffs.", + "Variables 2 and 4 will be numbers (a line number).", + "Variables 3 and 5 will be \"no lines changed\", \"1 line changed\" or \"X lines changed\", localized separately." + ] + }, + "blankLine", + { + "key": "unchangedLine", + "comment": [ + "The placeholders are contents of the line and should not be translated." + ] + }, + "equalLine", + "insertLine", + "deleteLine" + ], + "vs/editor/browser/widget/diffEditor/features/revertButtonsFeature": [ + "revertSelectedChanges", + "revertChange" + ], + "vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature": [ + "codeMovedToWithChanges", + "codeMovedFromWithChanges", + "codeMovedTo", + "codeMovedFrom" + ], + "vs/editor/browser/widget/diffEditor/components/diffEditorEditors": [ + "diff-aria-navigation-tip" ], "vs/workbench/browser/parts/editor/editorPlaceholder": [ "trustRequiredEditor", @@ -15979,52 +17551,53 @@ "unknownErrorEditorTextWithoutError", "retry" ], - "vs/workbench/contrib/comments/browser/commentColors": [ - "resolvedCommentIcon", - "unresolvedCommentIcon", - "resolvedCommentBorder", - "unresolvedCommentBorder", - "commentThreadRangeBackground", - "commentThreadActiveRangeBackground" - ], - "vs/base/browser/ui/menu/menubar": [ - "mAppMenu", - "mMore" + "vs/workbench/browser/parts/paneCompositeBar": [ + "resetLocation", + "resetLocation" ], - "vs/workbench/browser/parts/editor/multiEditorTabsControl": [ - "ariaLabelTabActions" + "vs/workbench/browser/parts/compositePart": [ + "ariaCompositeToolbarLabel", + "viewsAndMoreActions", + "titleTooltip" ], - "vs/workbench/browser/parts/editor/breadcrumbsControl": [ - "separatorIcon", - "breadcrumbsPossible", - "breadcrumbsVisible", - "breadcrumbsActive", - "empty", - "cmd.toggle", + "vs/workbench/browser/parts/editor/editorPanes": [ + "editorOpenErrorDialog", { - "key": "miBreadcrumbs", + "key": "ok", "comment": [ "&& denotes a mnemonic" ] + } + ], + "vs/workbench/browser/parts/editor/editorGroupWatermark": [ + "editorLineHighlight", + "watermark.showCommands", + "watermark.quickAccess", + "watermark.openFile", + "watermark.openFolder", + "watermark.openFileFolder", + "watermark.openRecent", + "watermark.newUntitledFile", + "watermark.findInFiles", + { + "key": "watermark.toggleTerminal", + "comment": [ + "toggle is a verb here" + ] }, - "cmd.toggle2", + "watermark.startDebugging", { - "key": "miBreadcrumbs2", + "key": "watermark.toggleFullscreen", "comment": [ - "&& denotes a mnemonic" + "toggle is a verb here" ] }, - "cmd.focusAndSelect", - "cmd.focus" + "watermark.showSettings" ], - "vs/platform/quickinput/browser/quickInput": [ - "quickInput.back", - "inputModeEntry", - "quickInput.steps", - "quickInputBox.ariaLabel", - "inputModeEntryDescription" + "vs/platform/quickinput/browser/quickInputUtils": [ + "executeCommand" ], - "vs/platform/quickinput/browser/quickInputList": [ + "vs/platform/quickinput/browser/quickInputTree": [ "quickInput" ], "vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators": [ @@ -16110,28 +17683,37 @@ "objectKeyHeader", "objectValueHeader" ], + "vs/workbench/contrib/chat/browser/chatAgentHover": [ + "marketplaceLabel" + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatWidget": [ + "inlineChat.accessibilityHelp", + "inlineChat.accessibilityHelpNoKb", + "aria-label", + "original", + "modified" + ], "vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer": [ "cellExecutionOrderCountLabel" ], - "vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll": [ - "toggleStickyScroll", - { - "key": "mitoggleStickyScroll", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "notebookStickyScroll", - { - "key": "miNotebookStickyScroll", - "comment": [ - "&& denotes a mnemonic" - ] - } + "vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider": [ + "notebookTreeAriaLabelHelp", + "notebookTreeAriaLabelHelpNoKb", + "notebookTreeAriaLabel" ], "vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager": [ "join.fileWorkingCopyManager" ], + "vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory": [ + "empty" + ], + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource": [ + "notebook.indexedChildrenLimitReached" + ], + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree": [ + "debugConsole", + "notebookVariableAriaLabel" + ], "vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget": [ "label.find", "placeholder.find", @@ -16150,16 +17732,8 @@ "notebook.find.filter.findInCodeInput", "notebook.find.filter.findInCodeOutput" ], - "vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory": [ - "empty" - ], - "vs/platform/actions/browser/buttonbar": [ - "labelWithKeybinding" - ], - "vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget": [ - "exited slash command mode" - ], "vs/workbench/contrib/terminal/browser/xterm/decorationAddon": [ + "workbench.action.terminal.toggleVisibility", "terminal.rerunCommand", "rerun", "yes", @@ -16170,26 +17744,11 @@ "terminal.copyOutputAsHtml", "workbench.action.terminal.runRecentCommand", "workbench.action.terminal.goToRecentDirectory", - "terminal.configureCommandDecorations", "terminal.learnShellIntegration", "toggleVisibility", - "toggleVisibility", "gutter", "overviewRuler" ], - "vs/workbench/contrib/debug/common/debugger": [ - "cannot.find.da", - "launch.config.comment1", - "launch.config.comment2", - "launch.config.comment3", - "debugType", - "debugTypeNotRecognised", - "node2NotSupported", - "debugRequest", - "debugWindowsConfiguration", - "debugOSXConfiguration", - "debugLinuxConfiguration" - ], "vs/workbench/contrib/debug/common/debugSchemas": [ "vscode.extension.contributes.debuggers", "vscode.extension.contributes.debuggers.type", @@ -16231,11 +17790,23 @@ "app.launch.json.compound.folder", "app.launch.json.compounds.configurations", "app.launch.json.compound.stopAll", - "compoundPrelaunchTask" + "compoundPrelaunchTask", + "debugger name", + "debugger type", + "debuggers" ], - "vs/workbench/contrib/mergeEditor/browser/mergeMarkers/mergeMarkersController": [ - "conflictingLine", - "conflictingLines" + "vs/workbench/contrib/debug/common/debugger": [ + "cannot.find.da", + "launch.config.comment1", + "launch.config.comment2", + "launch.config.comment3", + "debugType", + "debugTypeNotRecognised", + "node2NotSupported", + "debugRequest", + "debugWindowsConfiguration", + "debugOSXConfiguration", + "debugLinuxConfiguration" ], "vs/workbench/contrib/debug/browser/rawDebugSession": [ "noDebugAdapterStart", @@ -16249,6 +17820,15 @@ "noDebugAdapter", "moreInfo" ], + "vs/workbench/contrib/comments/browser/commentThreadWidget": [ + "commentLabel", + "commentLabelWithKeybinding", + "commentLabelWithKeybindingNoKeybinding" + ], + "vs/workbench/contrib/mergeEditor/browser/mergeMarkers/mergeMarkersController": [ + "conflictingLine", + "conflictingLines" + ], "vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel": [ "setInputHandled", "undoMarkAsHandled" @@ -16276,45 +17856,13 @@ "resetToBase", "resetToBaseTooltip" ], - "vs/workbench/contrib/comments/browser/commentGlyphWidget": [ - "editorGutterCommentRangeForeground", - "editorOverviewRuler.commentForeground", - "editorOverviewRuler.commentUnresolvedForeground", - "editorGutterCommentGlyphForeground", - "editorGutterCommentUnresolvedGlyphForeground" - ], - "vs/workbench/contrib/terminal/browser/terminalConfigHelper": [ - "useWslExtension.title", - "install" - ], - "vs/workbench/contrib/customEditor/common/extensionPoint": [ - "contributes.customEditors", - "contributes.viewType", - "contributes.displayName", - "contributes.selector", - "contributes.selector.filenamePattern", - "contributes.priority", - "contributes.priority.default", - "contributes.priority.option" - ], "vs/workbench/contrib/terminal/browser/terminalInstance": [ - "terminalTypeTask", - "terminalTypeLocal", "terminal.integrated.a11yPromptLabel", "terminal.integrated.useAccessibleBuffer", "terminal.integrated.useAccessibleBufferNoKb", "bellStatus", "keybindingHandling", "configureTerminalSettings", - "preview", - "confirmMoveTrashMessageFilesAndDirectories", - { - "key": "multiLinePasteButton", - "comment": [ - "&& denotes a mnemonic" - ] - }, - "doNotAskAgain", "disconnectStatus", "workspaceNotTrustedCreateTerminal", "workspaceNotTrustedCreateTerminalCwd", @@ -16329,7 +17877,6 @@ "setTerminalDimensionsColumn", "setTerminalDimensionsRow", "terminalStaleTextBoxAriaLabel", - "changeIcon", "changeColor", "launchFailed.exitCodeAndCommandLine", "launchFailed.exitCodeOnly", @@ -16337,6 +17884,20 @@ "terminated.exitCodeOnly", "launchFailed.errorMessage" ], + "vs/workbench/contrib/customEditor/common/extensionPoint": [ + "contributes.customEditors", + "contributes.viewType", + "contributes.displayName", + "contributes.selector", + "contributes.selector.filenamePattern", + "contributes.priority", + "contributes.priority.default", + "contributes.priority.option", + "customEditors view type", + "customEditors priority", + "customEditors filenamePattern", + "customEditors" + ], "vs/workbench/contrib/terminal/browser/terminalProfileQuickpick": [ "terminal.integrated.selectProfileToCreate", "terminal.integrated.chooseDefaultProfile", @@ -16378,6 +17939,24 @@ "openFolder", "followLink" ], + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget": [ + "default.placeholder", + "welcome.1" + ], + "vs/workbench/contrib/terminal/browser/xterm/decorationStyles": [ + "terminalPromptContextMenu", + "terminalPromptCommandFailed.duration", + "terminalPromptCommandFailedWithExitCode.duration", + "terminalPromptCommandSuccess.duration", + "terminalPromptCommandFailed", + "terminalPromptCommandFailedWithExitCode", + "terminalPromptCommandSuccess" + ], + "vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay": [ + "stickyScrollHoverTitle", + "labelWithKeybinding", + "labelWithKeybinding" + ], "vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget": [ "label.find", "placeholder.find", @@ -16390,18 +17969,37 @@ "ariaSearchNoResultWithLineNumNoCurrentMatch", "simpleFindWidget.sashBorder" ], - "vs/workbench/contrib/terminal/browser/xterm/decorationStyles": [ - "terminalPromptContextMenu", - "terminalPromptCommandFailed", - "terminalPromptCommandFailedWithExitCode", - "terminalPromptCommandSuccess" + "vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu": [ + "undo", + "redo", + "cut", + "copy", + "paste", + "selectAll" ], - "vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker": [ - "dark", - "light", - "HighContrast", - "HighContrastLight", - "seeMore" + "vs/workbench/services/suggest/browser/simpleSuggestWidget": [ + "suggest", + "label.full", + "label.detail", + "label.desc", + "ariaCurrenttSuggestionReadDetails" + ], + "vs/workbench/contrib/markdown/browser/markdownSettingRenderer": [ + "viewInSettings", + "viewInSettingsDetailed", + "restorePreviousValue", + "trueMessage", + "falseMessage", + "stringValue", + "numberValue", + "changeSettingTitle", + "copySettingId", + "copySettingId" + ], + "vs/workbench/contrib/welcomeGettingStarted/common/media/notebookProfile": [ + "default", + "jupyter", + "colab" ], "vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView": [ "explanation", @@ -16423,50 +18021,57 @@ "Theirs", "Yours" ], - "vs/workbench/contrib/welcomeGettingStarted/common/media/notebookProfile": [ - "default", - "jupyter", - "colab" + "vs/platform/languagePacks/common/localizedStrings": [ + "open", + "close", + "find" ], "vs/workbench/browser/parts/notifications/notificationsViewer": [ "executeCommand", "notificationActions", + "turnOnNotifications", + "turnOffNotifications", "notificationSource" ], - "vs/platform/languagePacks/common/localizedStrings": [ - "open", - "close", - "find" + "vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker": [ + "dark", + "light", + "HighContrast", + "HighContrastLight", + "seeMore" + ], + "vs/base/browser/ui/menu/menubar": [ + "mAppMenu", + "mMore" + ], + "vs/workbench/browser/parts/compositeBarActions": [ + "titleKeybinding", + "badgeTitle", + "additionalViews", + "numberBadge", + "manageExtension", + "hide", + "keep", + "hideBadge", + "showBadge", + "toggle", + "toggleBadge" ], "vs/editor/common/viewLayout/viewLineRenderer": [ "showMore", "overflow.chars" ], - "vs/editor/browser/widget/diffEditor/inlineDiffDeletedCodeMargin": [ - "diff.clipboard.copyDeletedLinesContent.label", - "diff.clipboard.copyDeletedLinesContent.single.label", - "diff.clipboard.copyChangedLinesContent.label", - "diff.clipboard.copyChangedLinesContent.single.label", - "diff.clipboard.copyDeletedLineContent.label", - "diff.clipboard.copyChangedLineContent.label", - "diff.inline.revertChange.label" - ], - "vs/editor/browser/widget/diffEditor/decorations": [ - "diffInsertIcon", - "diffRemoveIcon", - "revertChangeHoverMessage" - ], "vs/platform/actionWidget/browser/actionList": [ { "key": "label-preview", "comment": [ - "placeholders are keybindings, e.g \"F2 to apply, Shift+F2 to preview\"" + "placeholders are keybindings, e.g \"F2 to Apply, Shift+F2 to Preview\"" ] }, { "key": "label", "comment": [ - "placeholder is a keybinding, e.g \"F2 to apply\"" + "placeholder is a keybinding, e.g \"F2 to Apply\"" ] }, { @@ -16487,9 +18092,121 @@ "referenceCount", "treeAriaLabel" ], - "vs/workbench/browser/parts/editor/editorTabsControl": [ - "ariaLabelEditorActions", - "draggedEditorGroup" + "vs/workbench/browser/parts/compositeBar": [ + "activityBarAriaLabel" + ], + "vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/inlineDiffDeletedCodeMargin": [ + "diff.clipboard.copyDeletedLinesContent.label", + "diff.clipboard.copyDeletedLinesContent.single.label", + "diff.clipboard.copyChangedLinesContent.label", + "diff.clipboard.copyChangedLinesContent.single.label", + "diff.clipboard.copyDeletedLineContent.label", + "diff.clipboard.copyChangedLineContent.label", + "diff.inline.revertChange.label" + ], + "vs/workbench/browser/parts/editor/breadcrumbsControl": [ + "separatorIcon", + "breadcrumbsPossible", + "breadcrumbsVisible", + "breadcrumbsActive", + "empty", + { + "key": "miBreadcrumbs", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "cmd.toggle2", + { + "key": "miBreadcrumbs2", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "cmd.toggle", + "cmd.focusAndSelect", + "cmd.focus" + ], + "vs/workbench/browser/parts/editor/multiEditorTabsControl": [ + "ariaLabelTabActions" + ], + "vs/platform/actions/browser/buttonbar": [ + "labelWithKeybinding" + ], + "vs/workbench/contrib/notebook/browser/diff/diffElementOutputs": [ + "mimeTypePicker", + "empty", + "noRenderer.2", + "curruentActiveMimeType", + "promptChooseMimeTypeInSecure.placeHolder", + "promptChooseMimeType.placeHolder", + "builtinRenderInfo" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions": [ + "notebook.lineNumbers", + "notebook.showLineNumbers", + "notebook.cell.toggleLineNumbers.title", + "notebook.toggleLineNumbers" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/codeCell": [ + "cellExpandInputButtonLabelWithDoubleClick", + "cellExpandInputButtonLabel" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar": [ + "notebook.moreRunActionsLabel" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint": [ + "hiddenCellsLabel", + "hiddenCellsLabelPlural" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellOutput": [ + "cellOutputsCollapsedMsg", + "cellExpandOutputButtonLabelWithDoubleClick", + "cellExpandOutputButtonLabel" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/markupCell": [ + "cellExpandInputButtonLabelWithDoubleClick", + "cellExpandInputButtonLabel" + ], + "vs/workbench/contrib/comments/browser/commentThreadHeader": [ + "collapseIcon", + "label.collapse", + "startThread" + ], + "vs/workbench/contrib/comments/browser/commentThreadBody": [ + "commentThreadAria.withRange", + "commentThreadAria.document", + "commentThreadAria" + ], + "vs/workbench/contrib/terminal/browser/terminalProcessManager": [ + "killportfailure", + "ptyHostRelaunch" + ], + "vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick": [ + "removeCommand", + "viewCommandOutput", + "selectRecentCommandMac", + "selectRecentCommand", + "shellFileHistoryCategory", + "selectRecentDirectoryMac", + "selectRecentDirectory" + ], + "vs/workbench/contrib/terminal/common/terminalClipboard": [ + "preview", + "confirmMoveTrashMessageFilesAndDirectories", + { + "key": "multiLinePasteButton", + "comment": [ + "&& denotes a mnemonic" + ] + }, + { + "key": "multiLinePasteButton.oneLine", + "comment": [ + "&& denotes a mnemonic" + ] + }, + "doNotAskAgain" ], "vs/workbench/browser/parts/editor/breadcrumbs": [ "title", @@ -16537,95 +18254,31 @@ "vs/workbench/browser/parts/editor/breadcrumbsPicker": [ "breadcrumbs" ], - "vs/platform/quickinput/browser/quickInputUtils": [ - "executeCommand" - ], - "vs/workbench/contrib/notebook/browser/diff/diffElementOutputs": [ - "mimeTypePicker", + "vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput": [ "empty", "noRenderer.2", + "pickMimeType", "curruentActiveMimeType", + "installJupyterPrompt", "promptChooseMimeTypeInSecure.placeHolder", "promptChooseMimeType.placeHolder", - "builtinRenderInfo" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions": [ - "notebook.lineNumbers", - "notebook.toggleLineNumbers", - "notebook.showLineNumbers", - "notebook.cell.toggleLineNumbers.title" + "unavailableRenderInfo" ], - "vs/workbench/contrib/notebook/browser/view/cellParts/codeCell": [ - "cellExpandInputButtonLabelWithDoubleClick", - "cellExpandInputButtonLabel" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar": [ - "notebook.moreRunActionsLabel" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint": [ - "hiddenCellsLabel", - "hiddenCellsLabelPlural" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/markupCell": [ - "cellExpandInputButtonLabelWithDoubleClick", - "cellExpandInputButtonLabel" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellOutput": [ - "cellOutputsCollapsedMsg", - "cellExpandOutputButtonLabelWithDoubleClick", - "cellExpandOutputButtonLabel" - ], - "vs/workbench/services/suggest/browser/simpleSuggestWidget": [ - "suggest", - "label.full", - "label.detail", - "label.desc", - "ariaCurrenttSuggestionReadDetails" - ], - "vs/workbench/contrib/comments/browser/commentThreadWidget": [ - "commentLabel", - "commentLabelWithKeybinding", - "commentLabelWithKeybindingNoKeybinding" - ], - "vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick": [ - "removeCommand", - "viewCommandOutput", - "selectRecentCommandMac", - "selectRecentCommand", - "shellFileHistoryCategory", - "selectRecentDirectoryMac", - "selectRecentDirectory" - ], - "vs/workbench/contrib/terminal/browser/terminalProcessManager": [ - "killportfailure", - "ptyHostRelaunch" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput": [ - "empty", - "noRenderer.2", - "pickMimeType", - "curruentActiveMimeType", - "installJupyterPrompt", - "promptChooseMimeTypeInSecure.placeHolder", - "promptChooseMimeType.placeHolder", - "unavailableRenderInfo" + "vs/workbench/contrib/comments/browser/commentNode": [ + "commentToggleReaction", + "commentToggleReactionError", + "commentToggleReactionDefaultError", + "commentDeleteReactionError", + "commentDeleteReactionDefaultError", + "commentAddReactionError", + "commentAddReactionDefaultError" ], "vs/workbench/contrib/notebook/browser/view/cellParts/codeCellExecutionIcon": [ "notebook.cell.status.success", - "notebook.cell.status.failed", + "notebook.cell.status.failure", "notebook.cell.status.pending", "notebook.cell.status.executing" ], - "vs/workbench/contrib/comments/browser/commentThreadBody": [ - "commentThreadAria.withRange", - "commentThreadAria.document", - "commentThreadAria" - ], - "vs/workbench/contrib/comments/browser/commentThreadHeader": [ - "collapseIcon", - "label.collapse", - "startThread" - ], "vs/workbench/contrib/terminal/browser/environmentVariableInfo": [ "extensionEnvironmentContributionInfoStale", "relaunchTerminalLabel", @@ -16633,15 +18286,6 @@ "showEnvironmentContributions", "ScopedEnvironmentContributionInfo" ], - "vs/workbench/contrib/comments/browser/commentNode": [ - "commentToggleReaction", - "commentToggleReactionError", - "commentToggleReactionDefaultError", - "commentDeleteReactionError", - "commentDeleteReactionDefaultError", - "commentAddReactionError", - "commentAddReactionDefaultError" - ], "vs/workbench/contrib/comments/browser/reactionsAction": [ "pickReactions", "comment.toggleableReaction", @@ -16667,6 +18311,22 @@ "The emoji is also a button so that the current user can also toggle their own emoji reaction.", "The first arg is localized message \"Toggle reaction\" or empty if the user doesn't have permission to toggle the reaction, the second is number of users who have reacted with that reaction, and the third is the name of the reaction." ] + }, + { + "key": "comment.reactionLessThanTen", + "comment": [ + "This is a tooltip for an emoji that is a \"reaction\" to a comment where the count of the reactions is less than or equal to 10.", + "The emoji is also a button so that the current user can also toggle their own emoji reaction.", + "The first arg is localized message \"Toggle reaction\" or empty if the user doesn't have permission to toggle the reaction, the second iis a list of the reactors, and the third is the name of the reaction." + ] + }, + { + "key": "comment.reactionMoreThanTen", + "comment": [ + "This is a tooltip for an emoji that is a \"reaction\" to a comment where the count of the reactions is less than or equal to 10.", + "The emoji is also a button so that the current user can also toggle their own emoji reaction.", + "The first arg is localized message \"Toggle reaction\" or empty if the user doesn't have permission to toggle the reaction, the second iis a list of the reactors, and the third is the name of the reaction." + ] } ] }, @@ -16688,9 +18348,6 @@ "vs/code/node/cliProcessMain": [ "CLI" ], - "vs/code/node/sharedProcess/sharedProcessMain": [ - "Shared" - ], "vs/code/electron-sandbox/processExplorer/processExplorerMain": [ "Process Name", "CPU (%)", @@ -16702,19 +18359,14 @@ "Copy All", "Debug" ], - "vs/workbench/electron-sandbox/desktop.main": [ - "Saving UI state" + "vs/code/node/sharedProcess/sharedProcessMain": [ + "Shared" ], "vs/workbench/electron-sandbox/desktop.contribution": [ - "New Window Tab", - "Show Previous Window Tab", - "Show Next Window Tab", - "Move Window Tab to New Window", - "Merge All Windows", - "Toggle Window Tabs Bar", "E&&xit", "Controls the timeout in seconds before giving up resolving the shell environment when the application is not already launched from a terminal. See our [documentation](https://go.microsoft.com/fwlink/?linkid=2149667) for more information.", "Window", + "Controls whether a confirmation dialog shows asking to save or discard an opened untitled workspace in the window when switching to another workspace. Disabling the confirmation dialog will always discard the untitled workspace.", "Open a new empty window.", "Focus the last active running instance.", "Controls whether a new empty window should open when starting a second instance without arguments or if the last running instance should get focus.\nNote that there can still be cases where this setting is ignored (e.g. when using the `--new-window` or `--reuse-window` command line option).", @@ -16725,7 +18377,8 @@ "Never reopen a window. Unless a folder or workspace is opened (e.g. from the command line), an empty window will appear.", "Controls how windows are being reopened after starting for the first time. This setting has no effect when the application is already running.", "Controls whether a window should restore to full screen mode if it was exited in full screen mode.", - "Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.", + "Adjust the default zoom level for all windows. Each increment above `0` (e.g. `1`) or below (e.g. `-1`) represents zooming `20%` larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity. See {0} for configuring if the 'Zoom In' and 'Zoom Out' commands apply the zoom level to all windows or only the active window.", + "Controls if the 'Zoom In' and 'Zoom Out' commands apply the zoom level to all windows or only the active window. See {0} for configuring a default zoom level for all windows.", "Open new windows in the center of the screen.", "Open new windows with same dimension as last active one.", "Open new windows with same dimension as last active one with an offset position.", @@ -16735,6 +18388,10 @@ "Controls whether closing the last editor should also close the window. This setting only applies for windows that do not show folders.", "If enabled, this setting will close the window when the application icon in the title bar is double-clicked. The window will not be able to be dragged by the icon. This setting is effective only if `#window.titleBarStyle#` is set to `custom`.", "Adjust the appearance of the window title bar to be native by the OS or custom. On Linux and Windows, this setting also affects the application and context menu appearances. Changes require a full restart to apply.", + "Automatically changes custom title bar visibility.", + "Hide custom titlebar in full screen. When not in full screen, automatically change custom title bar visibility.", + "Hide custom titlebar when `#window.titleBarStyle#` is set to `native`.", + "Adjust when the custom title bar should be shown. The custom title bar can be hidden when in full screen mode with `windowed`. The custom title bar can only be hidden in none full screen mode with `never` when `#window.titleBarStyle#` is set to `native`.", "Adjust the appearance of dialog windows.", "Enables macOS Sierra window tabs. Note that changes require a full restart to apply and that native tabs will disable a custom title bar style if configured.", "Controls if native full-screen should be used on macOS. Disable this option to prevent macOS from creating a new space when going full-screen.", @@ -16745,6 +18402,8 @@ "Keyboard", "Enables the macOS touchbar buttons on the keyboard if available.", "A set of identifiers for entries in the touchbar that should not show up (for example `workbench.action.navigateBack`).", + "If enabled, a dialog will ask for confirmation whenever a local file or workspace is about to open through a protocol handler.", + "If enabled, a dialog will ask for confirmation whenever a remote file or workspace is about to open through a protocol handler.", "The display Language to use. Picking a different language requires the associated language pack to be installed.", "Disables hardware acceleration. ONLY change this option if you encounter graphic issues.", "Allows to override the color profile to use. If you experience colors appear badly, try to set this to `srgb` and restart.", @@ -16753,7 +18412,18 @@ "Enable proposed APIs for a list of extension ids (such as `vscode.git`). Proposed APIs are unstable and subject to breaking without warning at any time. This should only be set for extension development and testing purposes.", "Log level to use. Default is 'info'. Allowed values are 'error', 'warn', 'info', 'debug', 'trace', 'off'.", "Disables the Chromium sandbox. This is useful when running VS Code as elevated on Linux and running under Applocker on Windows.", - "Forces the renderer to be accessible. ONLY change this if you are using a screen reader on Linux. On other platforms the renderer will automatically be accessible. This flag is automatically set if you have editor.accessibilitySupport: on." + "Ensures that an in-memory store will be used for secret storage instead of using the OS's credential store. This is often used when running VS Code extension tests or when you're experiencing difficulties with the credential store.", + "Forces the renderer to be accessible. ONLY change this if you are using a screen reader on Linux. On other platforms the renderer will automatically be accessible. This flag is automatically set if you have editor.accessibilitySupport: on.", + "Configures the backend used to store secrets on Linux. This argument is ignored on Windows & macOS.", + "New Window Tab", + "Show Previous Window Tab", + "Show Next Window Tab", + "Move Window Tab to New Window", + "Merge All Windows", + "Toggle Window Tabs Bar" + ], + "vs/workbench/electron-sandbox/desktop.main": [ + "Saving UI state" ], "vs/workbench/services/textfile/electron-sandbox/nativeTextFileService": [ "Saving text files" @@ -16763,6 +18433,7 @@ "Save your workspace if you plan to open it again.", "&&Save", "Do&&n't Save", + "Always discard untitled workspaces without asking", "Unable to save workspace '{0}'", "The workspace is already opened in another window. Please close that window first and then try again.", "Opening a multi-root workspace." @@ -16787,27 +18458,14 @@ "Local", "Remote" ], + "vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService": [ + "Backup working copies" + ], "vs/workbench/services/integrity/electron-sandbox/integrityService": [ "Your {0} installation appears to be corrupt. Please reinstall.", "More Information", "Don't Show Again" ], - "vs/workbench/contrib/files/electron-sandbox/fileActions.contribution": [ - "Reveal in File Explorer", - "Reveal in Finder", - "Open Containing Folder", - "Share", - "File" - ], - "vs/workbench/services/voiceRecognition/electron-sandbox/workbenchVoiceRecognitionService": [ - "Voice Transcription", - "Getting microphone ready...", - "Recording from microphone...", - "Voice transcription failed: {0}" - ], - "vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService": [ - "Backup working copies" - ], "vs/workbench/services/extensions/electron-sandbox/nativeExtensionService": [ "Extension host cannot start: version mismatch.", "Relaunch VS Code", @@ -16823,49 +18481,61 @@ "Extension '{0}' is required to open the remote window.\nDo you want to install the extension?", "Install and Reload", "`{0}` not found on marketplace", - "Restart Extension Host", - "Restarting extension host on explicit request." + "Restarting extension host on explicit request.", + "Restart Extension Host" ], - "vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution": [ - "Running Extensions" + "vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService": [ + "Try saving or reverting the editors with unsaved changes first and then try again." ], "vs/workbench/contrib/localization/electron-sandbox/localization.contribution": [ "Would you like to change {0}'s display language to {1} and restart?", "Change Language and Restart", "Don't Show Again" ], - "vs/workbench/contrib/remote/electron-sandbox/remote.contribution": [ - "Whether the platform has the WSL feature installed", - "Remote", - "When enabled extensions are downloaded locally and installed on remote." + "vs/workbench/contrib/files/electron-sandbox/fileActions.contribution": [ + "Share", + "Reveal in File Explorer", + "Reveal in Finder", + "Open Containing Folder", + "File" + ], + "vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution": [ + "Running Extensions" ], "vs/workbench/contrib/issue/electron-sandbox/issue.contribution": [ - "Report Performance Issue...", - "Open Process Explorer", + "Type the name of an extension to report on.", + "Open Issue Reporter", "Open &&Process Explorer", - "Stop Tracing", "Tracing requires to launch with a '--trace' argument", "&&Relaunch and Enable Tracing", "Creating trace file...", - "This can take up to one minute to complete." + "This can take up to one minute to complete.", + "Report Performance Issue...", + "Open Process Explorer", + "Stop Tracing" + ], + "vs/workbench/contrib/remote/electron-sandbox/remote.contribution": [ + "Whether the platform has the WSL feature installed", + "Remote", + "When enabled extensions are downloaded locally and installed on remote." + ], + "vs/workbench/services/themes/electron-sandbox/themes.contribution": [ + "Native widget colors match the system colors.", + "Use light native widget colors for light color themes and dark for dark color themes.", + "Use light native widget colors.", + "Use dark native widget colors.", + "Set the color mode for native UI elements such as native dialogs, menus and title bar. Even if your OS is configured in light color mode, you can select a dark system color theme for the window. You can also configure to automatically adjust based on the {0} setting.\n\nNote: This setting is ignored when {1} is enabled." ], "vs/workbench/contrib/userDataSync/electron-sandbox/userDataSync.contribution": [ - "Open Local Backups Folder", "Local backups folder does not exist", "Successfully downloaded Settings Sync activity.", - "Open Folder" - ], - "vs/workbench/contrib/tasks/electron-sandbox/taskService": [ - "There is a task running. Do you want to terminate it?", - "&&Terminate Task", - "The launched task doesn't exist anymore. If the task spawned background processes exiting VS Code might result in orphaned processes. To avoid this start the last background process with a wait flag.", - "&&Exit Anyways" + "Open Folder", + "Open Local Backups Folder" ], "vs/workbench/contrib/performance/electron-sandbox/performance.contribution": [ "When enabled slow renderers are automatically profiled" ], "vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution": [ - "Open New External Terminal", "External Terminal", "Use VS Code's integrated terminal.", "Use the configured external terminal.", @@ -16877,10 +18547,19 @@ "When opening a repository from the Source Control Repositories view in a terminal, determines what kind of terminal will be launched", "Customizes which terminal to run on Windows.", "Customizes which terminal application to run on macOS.", - "Customizes which terminal to run on Linux." + "Customizes which terminal to run on Linux.", + "Open New External Terminal" + ], + "vs/workbench/contrib/tasks/electron-sandbox/taskService": [ + "There is a task running. Do you want to terminate it?", + "&&Terminate Task", + "The launched task doesn't exist anymore. If the task spawned background processes exiting VS Code might result in orphaned processes. To avoid this start the last background process with a wait flag.", + "&&Exit Anyways" + ], + "vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution": [ + "Multi Diff Editor" ], "vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution": [ - "Remote Tunnels", "Turn on Remote Tunnel Access...", "Turn off Remote Tunnel Access...", "Show Remote Tunnel Service Log", @@ -16920,15 +18599,76 @@ "Change Tunnel Name", "The name under which the remote tunnel access is registered. If not set, the host name is used.", "The name must only consist of letters, numbers, underscore and dash. It must not start with a dash.", - "Prevent the computer from sleeping when remote tunnel access is turned on." + "Prevent this computer from sleeping when remote tunnel access is turned on.", + "Remote Tunnels" ], "vs/base/common/platform": [ "_" ], + "vs/platform/environment/node/argv": [ + "Options", + "Extensions Management", + "Troubleshooting", + "Directory where CLI metadata should be stored.", + "Directory where CLI metadata should be stored.", + "Compare two files with each other.", + "Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.", + "Add folder(s) to the last active window.", + "Open a file at the path on the specified line and character position.", + "Force to open a new window.", + "Force to open a file or folder in an already opened window.", + "Wait for the files to be closed before returning.", + "The locale to use (e.g. en-US or zh-TW).", + "Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code.", + "Opens the provided folder or workspace with the given profile and associates the profile with the workspace. If the profile does not exist, a new empty one is created.", + "Print usage.", + "Set the root path for extensions.", + "List the installed extensions.", + "Show versions of installed extensions, when using --list-extensions.", + "Filters installed extensions by provided category, when using --list-extensions.", + "Installs or updates an extension. The argument is either an extension id or a path to a VSIX. The identifier of an extension is '${publisher}.${name}'. Use '--force' argument to update to latest version. To install a specific version provide '@${version}'. For example: 'vscode.csharp@1.2.3'.", + "Installs the pre-release version of the extension, when using --install-extension", + "Uninstalls an extension.", + "Update the installed extensions.", + "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.", + "Print version.", + "Print verbose output (implies --wait).", + "Log level to use. Default is 'info'. Allowed values are 'critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'. You can also configure the log level of an extension by passing extension id and log level in the following format: '${publisher}.${name}:${logLevel}'. For example: 'vscode.csharp:trace'. Can receive one or more such entries.", + "Print process usage and diagnostics information.", + "Run CPU profiler during startup.", + "Disable all installed extensions. This option is not persisted and is effective only when the command opens a new window.", + "Disable the provided extension. This option is not persisted and is effective only when the command opens a new window.", + "Turn sync on or off.", + "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.", + "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.", + "Disable GPU hardware acceleration.", + "Use this option only when there is requirement to launch the application as sudo user on Linux or when running as an elevated user in an applocker environment on Windows.", + "Shows all telemetry events which VS code collects.", + "Use {0} instead.", + "paths", + "Usage", + "options", + "To read output from another program, append '-' (e.g. 'echo Hello World | {0} -')", + "To read from stdin, append '-' (e.g. 'ps aux | grep code | {0} -')", + "Subcommands", + "Unknown version", + "Unknown commit" + ], + "vs/platform/log/common/log": [ + "Trace", + "Debug", + "Info", + "Warning", + "Error", + "Off" + ], + "vs/platform/terminal/node/ptyService": [ + "History restored" + ], "vs/editor/common/config/editorOptions": [ - "Use platform APIs to detect when a Screen Reader is attached", - "Optimize for usage with a Screen Reader", - "Assume a screen reader is not attached", + "Use platform APIs to detect when a Screen Reader is attached.", + "Optimize for usage with a Screen Reader.", + "Assume a screen reader is not attached.", "Controls if the UI should run in a mode where it is optimized for screen readers.", "Controls whether a space character is inserted when commenting.", "Controls if empty lines should be ignored with toggle, add or remove actions for line comments.", @@ -16971,16 +18711,19 @@ "Controls whether the hover is shown.", "Controls the delay in milliseconds after which the hover is shown.", "Controls whether the hover should remain visible when mouse is moved over it.", - "Controls the delay in milliseconds after thich the hover is hidden. Requires `editor.hover.sticky` to be enabled.", + "Controls the delay in milliseconds after which the hover is hidden. Requires `editor.hover.sticky` to be enabled.", "Prefer showing hovers above the line, if there's space.", "Assumes that all characters are of the same width. This is a fast algorithm that works correctly for monospace fonts and certain scripts (like Latin characters) where glyphs are of equal width.", "Delegates wrapping points computation to the browser. This is a slow algorithm, that might cause freezes for large files, but it works correctly in all cases.", "Controls the algorithm that computes wrapping points. Note that when in accessibility mode, advanced will be used for the best experience.", + "Disable the code action menu.", + "Show the code action menu when the cursor is on lines with code.", + "Show the code action menu when the cursor is on lines with code or on empty lines.", "Enables the Code Action lightbulb in the editor.", "Shows the nested current scopes during the scroll at the top of the editor.", "Defines the maximum number of sticky lines to show.", "Defines the model to use for determining which lines to stick. If the outline model does not exist, it will fall back on the folding provider model which falls back on the indentation model. This order is respected in all three cases.", - "Enable scrolling of the sticky scroll widget with the editor's horizontal scrollbar.", + "Enable scrolling of Sticky Scroll with the editor's horizontal scrollbar.", "Enables the inlay hints in the editor.", "Inlay hints are enabled", "Inlay hints are showing by default and hide when holding {0}", @@ -17001,6 +18744,9 @@ "Scale of content drawn in the minimap: 1, 2 or 3.", "Render the actual characters on a line as opposed to color blocks.", "Limit the width of the minimap to render at most a certain number of columns.", + "Controls whether named regions are shown as section headers in the minimap.", + "Controls whether MARK: comments are shown as section headers in the minimap.", + "Controls the font size of section headers in the minimap.", "Controls the amount of space between the top edge of the editor and the first line.", "Controls the amount of space between the bottom edge of the editor and the last line.", "Enables a pop-up that shows parameter documentation and type information as you type.", @@ -17031,6 +18777,7 @@ "The width of the vertical scrollbar.", "The height of the horizontal scrollbar.", "Controls whether clicks scroll by page or jump to click position.", + "When set, the horizontal scrollbar will not increase the size of the editor's content.", "Controls whether all non-basic ASCII characters are highlighted. Only characters between U+0020 and U+007E, tab, line-feed and carriage-return are considered basic ASCII.", "Controls whether characters that just reserve space or have no width at all are highlighted.", "Controls whether characters are highlighted that can be confused with basic ASCII characters, except those that are common in the current user locale.", @@ -17041,8 +18788,17 @@ "Controls whether to automatically show inline suggestions in the editor.", "Show the inline suggestion toolbar whenever an inline suggestion is shown.", "Show the inline suggestion toolbar when hovering over an inline suggestion.", + "Never show the inline suggestion toolbar.", "Controls when to show the inline suggestion toolbar.", "Controls how inline suggestions interact with the suggest widget. If enabled, the suggest widget is not shown automatically when inline suggestions are available.", + "Controls the font family of the inline suggestions.", + "Controls whether to show inline edits in the editor.", + "Show the inline edit toolbar whenever an inline suggestion is shown.", + "Show the inline edit toolbar when hovering over an inline suggestion.", + "Never show the inline edit toolbar.", + "Controls when to show the inline edit toolbar.", + "Controls the font family of the inline edit.", + "Controls whether to color the background of inline edits.", "Controls whether bracket pair colorization is enabled or not. Use {0} to override the bracket highlight colors.", "Controls whether each bracket type has its own independent color pool.", "Enables bracket pair guides.", @@ -17109,12 +18865,14 @@ "When enabled IntelliSense shows `issues`-suggestions.", "Whether leading and trailing whitespace should always be selected.", "Whether subwords (like 'foo' in 'fooBar' or 'foo_bar') should be selected.", + "Locales to be used for word segmentation when doing word related navigations or operations. Specify the BCP 47 language tag of the word you wish to recognize (e.g., ja, zh-CN, zh-Hant-TW, etc.).", + "Locales to be used for word segmentation when doing word related navigations or operations. Specify the BCP 47 language tag of the word you wish to recognize (e.g., ja, zh-CN, zh-Hant-TW, etc.).", "No indentation. Wrapped lines begin at column 1.", "Wrapped lines get the same indentation as the parent.", "Wrapped lines get +1 indentation toward the parent.", "Wrapped lines get +2 indentation toward the parent.", "Controls the indentation of wrapped lines.", - "Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor).", + "Controls whether you can drag and drop a file into a text editor by holding down the `Shift` key (instead of opening the file in an editor).", "Controls if a widget is shown when dropping files into the editor. This widget lets you control how the file is dropped.", "Show the drop selector widget after a file is dropped into the editor.", "Never show the drop selector widget. Instead the default drop provider is always used.", @@ -17172,7 +18930,7 @@ "Controls the minimal number of visible leading lines (minimum 0) and trailing lines (minimum 1) surrounding the cursor. Known as 'scrollOff' or 'scrollOffset' in some other editors.", "`cursorSurroundingLines` is enforced only when triggered via the keyboard or API.", "`cursorSurroundingLines` is enforced always.", - "Controls when `#cursorSurroundingLines#` should be enforced.", + "Controls when `#editor.cursorSurroundingLines#` should be enforced.", "Controls the width of the cursor when `#editor.cursorStyle#` is set to `line`.", "Controls whether the editor should allow moving selections via drag and drop.", "Use a new rendering method with svgs.", @@ -17198,6 +18956,7 @@ "Controls whether the editor should detect links and make them clickable.", "Highlight matching brackets.", "A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events.", + "Zoom the font of the editor when using mouse wheel and holding `Cmd`.", "Zoom the font of the editor when using mouse wheel and holding `Ctrl`.", "Merge multiple cursors when they are overlapping.", "Maps to `Control` on Windows and Linux and to `Command` on macOS.", @@ -17207,7 +18966,10 @@ "Each cursor pastes the full text.", "Controls pasting when the line count of the pasted text matches the cursor count.", "Controls the max number of cursors that can be in an active editor at once.", - "Controls whether the editor should highlight semantic symbol occurrences.", + "Does not highlight occurrences.", + "Highlights occurrences only in the current file.", + "Experimental: Highlights occurrences across all valid open files.", + "Controls whether occurrences should be highlighted across open files.", "Controls whether a border should be drawn around the overview ruler.", "Focus the tree when opening peek", "Focus the editor when opening peek", @@ -17259,7 +19021,7 @@ "Unusual line terminators are ignored.", "Unusual line terminators prompt to be removed.", "Remove unusual line terminators that might cause problems.", - "Inserting and deleting whitespace follows tab stops.", + "Spaces and tabs are inserted and deleted in alignment with tab stops.", "Use the default line break rule.", "Word breaks should not be used for Chinese/Japanese/Korean (CJK) text. Non-CJK text behavior is the same as for normal.", "Controls the word break rules used for Chinese/Japanese/Korean (CJK) text.", @@ -17273,57 +19035,6 @@ "Controls whether inline color decorations should be shown using the default document color provider", "Controls whether the editor receives tabs or defers them to the workbench for navigation." ], - "vs/platform/environment/node/argv": [ - "Options", - "Extensions Management", - "Troubleshooting", - "Directory where CLI metadata should be stored.", - "Directory where CLI metadata should be stored.", - "Compare two files with each other.", - "Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.", - "Add folder(s) to the last active window.", - "Open a file at the path on the specified line and character position.", - "Force to open a new window.", - "Force to open a file or folder in an already opened window.", - "Wait for the files to be closed before returning.", - "The locale to use (e.g. en-US or zh-TW).", - "Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code.", - "Opens the provided folder or workspace with the given profile and associates the profile with the workspace. If the profile does not exist, a new empty one is created. A folder or workspace must be provided for the profile to take effect.", - "Print usage.", - "Set the root path for extensions.", - "List the installed extensions.", - "Show versions of installed extensions, when using --list-extensions.", - "Filters installed extensions by provided category, when using --list-extensions.", - "Installs or updates an extension. The argument is either an extension id or a path to a VSIX. The identifier of an extension is '${publisher}.${name}'. Use '--force' argument to update to latest version. To install a specific version provide '@${version}'. For example: 'vscode.csharp@1.2.3'.", - "Installs the pre-release version of the extension, when using --install-extension", - "Uninstalls an extension.", - "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.", - "Print version.", - "Print verbose output (implies --wait).", - "Log level to use. Default is 'info'. Allowed values are 'critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'. You can also configure the log level of an extension by passing extension id and log level in the following format: '${publisher}.${name}:${logLevel}'. For example: 'vscode.csharp:trace'. Can receive one or more such entries.", - "Print process usage and diagnostics information.", - "Run CPU profiler during startup.", - "Disable all installed extensions. This option is not persisted and is effective only when the command opens a new window.", - "Disable the provided extension. This option is not persisted and is effective only when the command opens a new window.", - "Turn sync on or off.", - "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.", - "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.", - "Disable GPU hardware acceleration.", - "Use this option only when there is requirement to launch the application as sudo user on Linux or when running as an elevated user in an applocker environment on Windows.", - "Shows all telemetry events which VS code collects.", - "Use {0} instead.", - "paths", - "Usage", - "options", - "To read output from another program, append '-' (e.g. 'echo Hello World | {0} -')", - "To read from stdin, append '-' (e.g. 'ps aux | grep code | {0} -')", - "Subcommands", - "Unknown version", - "Unknown commit" - ], - "vs/platform/terminal/node/ptyService": [ - "History restored" - ], "vs/base/common/errorMessage": [ "{0}: {1}", "A system error occurred ({0})", @@ -17333,18 +19044,14 @@ "An unknown error occurred. Please consult the log for more details." ], "vs/code/electron-main/app": [ + "An external application wants to open '{0}' in {1}. Do you want to open this workspace file?", + "An external application wants to open '{0}' in {1}. Do you want to open this folder?", + "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", "&&Yes", "&&No", - "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", - "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'" - ], - "vs/platform/files/common/files": [ - "Unknown Error", - "{0}B", - "{0}KB", - "{0}MB", - "{0}GB", - "{0}TB" + "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'", + "Allow opening local paths without asking", + "Allow opening remote paths without asking" ], "vs/platform/environment/node/argvHelper": [ "Option '{0}' is defined more than once. Using value '{1}'.", @@ -17354,14 +19061,13 @@ "Warning: '{0}' is not in the list of known options, but still passed to Electron/Chromium.", "Arguments in `--goto` mode should be in the format of `FILE(:LINE(:CHARACTER))`." ], - "vs/platform/files/node/diskFileSystemProvider": [ - "File already exists", - "File does not exist", - "Unable to move '{0}' into '{1}' ({2}).", - "Unable to copy '{0}' into '{1}' ({2}).", - "File cannot be copied to same path with different path case", - "File to move/copy does not exist", - "File at target already exists and thus will not be moved/copied to unless overwrite is specified" + "vs/platform/files/common/files": [ + "Unknown Error", + "{0}B", + "{0}KB", + "{0}MB", + "{0}GB", + "{0}TB" ], "vs/platform/files/common/fileService": [ "Unable to resolve filesystem provider with relative file path '{0}'", @@ -17392,6 +19098,15 @@ "Unable to modify read-only file '{0}'", "Unable to modify read-only file '{0}'" ], + "vs/platform/files/node/diskFileSystemProvider": [ + "File already exists", + "File does not exist", + "Unable to move '{0}' into '{1}' ({2}).", + "Unable to copy '{0}' into '{1}' ({2}).", + "File cannot be copied to same path with different path case", + "File to move/copy does not exist", + "File at target already exists and thus will not be moved/copied to unless overwrite is specified" + ], "vs/platform/request/common/request": [ "Network Requests", "HTTP", @@ -17407,17 +19122,6 @@ "Controls whether CA certificates should be loaded from the OS. (On Windows and macOS, a reload of the window is required after turning this off.)", "Controls whether experimental loading of CA certificates from the OS should be enabled. This uses a more general approach than the default implemenation." ], - "vs/platform/dialogs/common/dialogs": [ - "&&Yes", - "Cancel", - "Cancel", - "Cancel", - "&&OK", - "&&OK", - "Cancel", - "...1 additional file not shown", - "...{0} additional files not shown" - ], "vs/platform/update/common/update.config.contribution": [ "Update", "Configure whether you receive automatic updates. Requires a restart after change. The updates are fetched from a Microsoft online service.", @@ -17431,104 +19135,45 @@ "Enable to download and install new VS Code versions in the background on Windows.", "Show Release Notes after an update. The Release Notes are fetched from a Microsoft online service." ], - "vs/code/electron-sandbox/issue/issueReporterPage": [ - "Include my system information", - "Include my currently running processes", - "Include my workspace metadata", - "Include my enabled extensions", - "Include A/B experiment info", - "Before you report an issue here please review the guidance we provide.", - "Please complete the form in English.", - "This is a", - "File on", - "An issue source is required.", - "Try to reproduce the problem after {0}. If the problem only reproduces when extensions are active, it is likely an issue with an extension.", - "disabling all extensions and reloading the window", - "Extension", - "The issue reporter is unable to create issues for this extension. Please visit {0} to report an issue.", - "The issue reporter is unable to create issues for this extension, as it does not specify a URL for reporting issues. Please check the marketplace page of this extension to see if other instructions are available.", - "Title", - "Please enter a title.", - "A title is required.", - "The title is too long.", - "Please enter details.", - "A description is required.", - "show", - "show", - "show", - "show", - "show" - ], - "vs/code/electron-sandbox/issue/issueReporterService": [ - "hide", - "show", - "Create on GitHub", - "Preview on GitHub", - "Loading data...", - "GitHub query limit exceeded. Please wait.", - "Similar issues", - "Open", - "Closed", - "Open", - "Closed", - "No similar issues found", - "Bug Report", - "Feature Request", - "Performance Issue", - "Select source", - "Visual Studio Code", - "An extension", - "Extensions marketplace", - "Don't know", - "This extension handles issues outside of VS Code", - "The '{0}' extension prefers to use an external issue reporter. To be taken to that issue reporting experience, click the button below.", - "Open External Issue Reporter", - "Steps to Reproduce", - "Share the steps needed to reliably reproduce the problem. Please include actual and expected results. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.", - "Steps to Reproduce", - "When did this performance issue happen? Does it occur on startup or after a specific series of actions? We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.", - "Description", - "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.", - "We have written the needed data into your clipboard because it was too large to send. Please paste.", - "Select extension", - "Extensions are disabled", - "No current experiments." + "vs/platform/dialogs/common/dialogs": [ + "&&Yes", + "Cancel", + "Cancel", + "Cancel", + "&&OK", + "&&OK", + "Cancel", + "...1 additional file not shown", + "...{0} additional files not shown" ], "vs/platform/extensionManagement/common/extensionManagement": [ "Extensions", "Preferences" ], - "vs/platform/extensionManagement/common/extensionsScannerService": [ - "Cannot read file {0}: {1}.", - "Failed to parse {0}: [{1}, {2}] {3}.", - "Invalid manifest file {0}: Not an JSON object.", - "Failed to parse {0}: {1}.", - "Invalid format {0}: JSON object expected.", - "Failed to parse {0}: {1}.", - "Invalid format {0}: JSON object expected." - ], - "vs/platform/languagePacks/common/languagePacks": [ - " (Current)" - ], "vs/platform/extensionManagement/common/extensionManagementCLI": [ "Extension '{0}' not found.", "Make sure you use the full extension ID, including the publisher, e.g.: {0}", "Extensions installed on {0}:", "Installing extensions on {0}...", "Installing extensions...", - "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", - "Extension '{0}' is already installed.", "Error while installing extensions: {0}", "Failed Installing Extensions: {0}", - "Extension '{0}' was successfully installed.", - "Cancelled installing extension '{0}'.", + "Fetching latest versions for {0} extensions", + "No extension to update", + "Updating extensions: {0}", + "Error while updating extension {0}: {1}", + "Extension '{0}' v{1} was successfully updated.", + "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", + "Extension '{0}' is already installed.", "Extension '{0}' is already installed.", "Updating the extension '{0}' to the version {1}", "Installing builtin extension '{0}' v{1}...", "Installing builtin extension '{0}'...", "Installing extension '{0}' v{1}...", "Installing extension '{0}'...", + "Error while installing extension {0}: {1}", "Extension '{0}' v{1} was successfully installed.", + "Extension '{0}' was successfully installed.", "Cancelled installing extension '{0}'.", "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", "Extension '{0}' is a Built-in extension and cannot be uninstalled", @@ -17539,6 +19184,15 @@ "Extension '{0}' is not installed on {1}.", "Extension '{0}' is not installed." ], + "vs/platform/extensionManagement/common/extensionsScannerService": [ + "Cannot read file {0}: {1}.", + "Failed to parse {0}: [{1}, {2}] {3}.", + "Invalid manifest file {0}: Not an JSON object.", + "Failed to parse {0}: {1}.", + "Invalid format {0}: JSON object expected.", + "Failed to parse {0}: {1}.", + "Invalid format {0}: JSON object expected." + ], "vs/platform/extensionManagement/node/extensionManagementService": [ "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", "Marketplace is not enabled", @@ -17550,6 +19204,9 @@ "Please restart VS Code before reinstalling {0}.", "Please restart VS Code before reinstalling {0}." ], + "vs/platform/languagePacks/common/languagePacks": [ + " (Current)" + ], "vs/platform/telemetry/common/telemetryService": [ "Controls {0} telemetry, first-party extension telemetry, and participating third-party extension telemetry. Some third party extensions might not respect this setting. Consult the specific extension's documentation to be sure. Telemetry helps us better understand how {0} is performing, where improvements need to be made, and how features are being used.", "Read more about the [data we collect]({0}).", @@ -17570,9 +19227,76 @@ "Enable diagnostic data to be collected. This helps us to better understand how {0} is performing and where improvements need to be made. [Read more]({1}) about what we collect and our privacy statement.", "If this setting is false, no telemetry will be sent regardless of the new setting's value. Deprecated in favor of the {0} setting." ], + "vs/code/electron-sandbox/issue/issueReporterPage": [ + "Include my system information", + "Include my currently running processes", + "Include my workspace metadata", + "Include my enabled extensions", + "Include A/B experiment info", + "Include additional extension info", + "Before you report an issue here please review the guidance we provide.", + "Please complete the form in English.", + "This is a", + "File on", + "An issue source is required.", + "Try to reproduce the problem after {0}. If the problem only reproduces when extensions are active, it is likely an issue with an extension.", + "disabling all extensions and reloading the window", + "Extension", + "The issue reporter is unable to create issues for this extension. Please visit {0} to report an issue.", + "The issue reporter is unable to create issues for this extension, as it does not specify a URL for reporting issues. Please check the marketplace page of this extension to see if other instructions are available.", + "Title", + "Please enter a title.", + "A title is required.", + "The title is too long.", + "Please enter details.", + "A description is required.", + "Please provide a longer description.", + "show", + "Extension does not have additional data to include.", + "show", + "show", + "show", + "show", + "show" + ], "vs/platform/userDataProfile/common/userDataProfile": [ "Default" ], + "vs/code/electron-sandbox/issue/issueReporterService": [ + "hide", + "show", + "Create on GitHub", + "Preview on GitHub", + "Loading data...", + "GitHub query limit exceeded. Please wait.", + "Similar issues", + "Open", + "Closed", + "Open", + "Closed", + "No similar issues found", + "Bug Report", + "Feature Request", + "Performance Issue", + "Select source", + "Visual Studio Code", + "An extension", + "Extensions marketplace", + "Don't know", + "This extension handles issues outside of VS Code", + "The '{0}' extension prefers to use an external issue reporter. To be taken to that issue reporting experience, click the button below.", + "Open External Issue Reporter", + "Steps to Reproduce", + "Share the steps needed to reliably reproduce the problem. Please include actual and expected results. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.", + "Steps to Reproduce", + "When did this performance issue happen? Does it occur on startup or after a specific series of actions? We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.", + "Description", + "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.", + "We have written the needed data into your clipboard because it was too large to send. Please paste.", + "Select extension", + "Extensions are disabled", + "No current experiments." + ], "vs/platform/telemetry/common/telemetryLogAppender": [ "Telemetry{0}" ], @@ -17583,18 +19307,18 @@ "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.", "Configure settings to be ignored while synchronizing." ], - "vs/platform/userDataSync/common/userDataSyncMachines": [ - "Cannot read machines data as the current version is incompatible. Please update {0} and try again." - ], "vs/platform/userDataSync/common/userDataSyncLog": [ "Settings Sync" ], - "vs/platform/userDataSync/common/userDataSyncResourceProvider": [ - "Cannot parse sync data as it is not compatible with the current version." + "vs/platform/userDataSync/common/userDataSyncMachines": [ + "Cannot read machines data as the current version is incompatible. Please update {0} and try again." ], "vs/platform/remoteTunnel/common/remoteTunnel": [ "Remote Tunnel Service" ], + "vs/platform/userDataSync/common/userDataSyncResourceProvider": [ + "Cannot parse sync data as it is not compatible with the current version." + ], "vs/platform/remoteTunnel/node/remoteTunnelService": [ "Building CLI from sources", "Connecting as {0} ({1})", @@ -17627,6 +19351,8 @@ "Use contiguous matching when searching.", "Controls the type of matching used when searching lists and trees in the workbench.", "Controls how tree folders are expanded when clicking the folder names. Note that some trees and lists might choose to ignore this setting if it is not applicable.", + "Controls whether sticky scrolling is enabled in trees.", + "Controls the number of sticky elements displayed in the tree when `#workbench.tree.enableStickyScroll#` is enabled.", "Controls how type navigation works in lists and trees in the workbench. When set to `trigger`, type navigation begins once the `list.triggerTypeNavigation` command is run." ], "vs/platform/markers/common/markers": [ @@ -17654,12 +19380,27 @@ "The default size.", "Increases the size, so it can be grabbed more easily with the mouse.", "Controls the height of the scrollbars used for tabs and breadcrumbs in the editor title area.", - "Controls whether opened editors should show in tabs or not.", - "Controls whether tabs should be wrapped over multiple lines when exceeding available space or whether a scrollbar should appear instead. This value is ignored when `#workbench.editor.showTabs#` is disabled.", - "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behavior for that duration. This value is ignored when `#workbench.editor.showTabs#` is disabled.", - "Controls whether a top border is drawn on tabs for editors that have unsaved changes. This value is ignored when `#workbench.editor.showTabs#` is disabled.", + "Each editor is displayed as a tab in the editor title area.", + "The active editor is displayed as a single large tab in the editor title area.", + "The editor title area is not displayed.", + "Controls whether opened editors should show as individual tabs, one single large tab or if the title area should not be shown.", + "Show editor actions in the window title bar when {0} is set to {1}. Otherwise, editor actions are shown in the editor tab bar.", + "Show editor actions in the window title bar. If {0} is set to {1}, editor actions are hidden.", + "Editor actions are not shown.", + "Controls where the editor actions are shown.", + "Controls whether tabs should be wrapped over multiple lines when exceeding available space or whether a scrollbar should appear instead. This value is ignored when {0} is not set to '{1}'.", + "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behavior for that duration. This value is ignored when {0} is not set to {1}.", + "Controls whether a top border is drawn on tabs for editors that have unsaved changes. This value is ignored when {0} is not set to {1}.", "Controls whether editor file decorations should use badges.", "Controls whether editor file decorations should use colors.", + "Controls whether the custom workbench editor labels should be applied.", + "Controls the rendering of the editor label. Each __Item__ is a pattern that matches a file path. Both relative and absolute file paths are supported. In case multiple patterns match, the longest matching path will be picked. Each __Value__ is the template for the rendered editor when the __Item__ matches. Variables are substituted based on the context:", + "`${dirname}`: name of the folder in which the file is located (e.g. `root/folder/file.txt -> folder`).", + "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=1: root/folder/file.txt -> root`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: root/folder/file.txt -> root`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absoulte path, otherwise it corresponds to the workspace folder.", + "`${filename}`: name of the file without the file extension (e.g. `root/folder/file.txt -> file`).", + "`${extname}`: the file extension (e.g. `root/folder/file.txt -> txt`).", + "Example: `\"**/static/**/*.html\": \"${filename} - ${dirname} (${extname})\"` will render a file `root/static/folder/file.html` as `file - folder (html)`.", + "The template which should be rendered when the pattern mtches. May include the variables ${dirname}, ${filename} and ${extname}.", "Show the name of the file. When tabs are enabled and two files have the same name in one group the distinguishing sections of each file's path are added. When tabs are disabled, the path relative to the workspace folder is shown if the editor is active.", "Show the name of the file followed by its directory name.", "Show the name of the file followed by its path relative to the workspace folder.", @@ -17675,19 +19416,21 @@ "When enabled, shows a Status bar Quick Fix when the editor language doesn't match detected content language.", "Show in untitled text editors", "Show in notebook editors", - "Controls the position of the editor's tabs close buttons, or disables them when set to 'off'. This value is ignored when `#workbench.editor.showTabs#` is disabled.", + "Controls the position of the editor's tabs action buttons (close, unpin). This value is ignored when {0} is not set to {1}.", + "Controls the visibility of the tab close action button.", + "Controls the visibility of the tab unpin action button.", "Always keep tabs large enough to show the full editor label.", "Allow tabs to get smaller when the available space is not enough to show all tabs at once.", "Make all tabs the same size, while allowing them to get smaller when the available space is not enough to show all tabs at once.", - "Controls the size of editor tabs. This value is ignored when `#workbench.editor.showTabs#` is disabled.", - "Controls the minimum width of tabs when `#workbench.editor.tabSizing#` size is set to `fixed`.", - "Controls the maximum width of tabs when `#workbench.editor.tabSizing#` size is set to `fixed`.", - "Controls the height of editor tabs. Also applies to the title control bar when `#workbench.editor.showTabs#` is disabled.", + "Controls the size of editor tabs. This value is ignored when {0} is not set to {1}.", + "Controls the minimum width of tabs when {0} size is set to {1}.", + "Controls the maximum width of tabs when {0} size is set to {1}.", + "Controls the height of editor tabs. Also applies to the title control bar when {0} is not set to {1}.", "A pinned tab inherits the look of non pinned tabs.", "A pinned tab will show in a compact form with only icon or first letter of the editor name.", "A pinned tab shrinks to a compact fixed size showing parts of the editor name.", - "Controls the size of pinned editor tabs. Pinned tabs are sorted to the beginning of all opened tabs and typically do not close until unpinned. This value is ignored when `#workbench.editor.showTabs#` is disabled.", - "When enabled, displays pinned tabs in a separate row above all other tabs. This value is ignored when `#workbench.editor.showTabs#` is disabled.", + "Controls the size of pinned editor tabs. Pinned tabs are sorted to the beginning of all opened tabs and typically do not close until unpinned. This value is ignored when {0} is not set to {1}.", + "When enabled, displays pinned tabs in a separate row above all other tabs. This value is ignored when {0} is not set to {1}.", "Always prevent closing the pinned editor when using mouse middle click or keyboard.", "Prevent closing the pinned editor when using the keyboard.", "Prevent closing the pinned editor when using mouse middle click.", @@ -17698,13 +19441,14 @@ "Splits the active editor group to equal parts.", "Controls the size of editor groups when splitting them.", "Controls if editor groups can be split from drag and drop operations by dropping an editor or file on the edges of the editor area.", + "Controls if editors can be dragged out of the window to open them in a new window. Press and hold the `Alt` key while dragging to toggle this dynamically.", "Controls whether editors are closed in most recently used order or from left to right.", "Controls whether opened editors should show with an icon or not. This requires a file icon theme to be enabled as well.", "Controls whether opened editors show as preview editors. Preview editors do not stay open, are reused until explicitly set to be kept open (via double-click or editing), and show file names in italics.", - "Controls whether editors opened from Quick Open show as preview editors. Preview editors do not stay open, and are reused until explicitly set to be kept open (via double-click or editing). When enabled, hold Ctrl before selection to open an editor as a non-preview. This value is ignored when `#workbench.editor.enablePreview#` is disabled.", - "Controls whether editors remain in preview when a code navigation is started from them. Preview editors do not stay open, and are reused until explicitly set to be kept open (via double-click or editing). This value is ignored when `#workbench.editor.enablePreview#` is disabled.", + "Controls whether editors opened from Quick Open show as preview editors. Preview editors do not stay open, and are reused until explicitly set to be kept open (via double-click or editing). When enabled, hold Ctrl before selection to open an editor as a non-preview. This value is ignored when {0} is not set to {1}.", + "Controls whether editors remain in preview when a code navigation is started from them. Preview editors do not stay open, and are reused until explicitly set to be kept open (via double-click or editing). This value is ignored when {0} is not set to {1}.", "Controls whether editors showing a file that was opened during the session should close automatically when getting deleted or renamed by some other process. Disabling this will keep the editor open on such an event. Note that deleting from within the application will always close the editor and that editors with unsaved changes will never close to preserve your data.", - "Controls where editors open. Select `left` or `right` to open editors to the left or right of the currently active one. Select `first` or `last` to open editors independently from the currently active one.", + "Controls where editors open. Select {0} or {1} to open editors to the left or right of the currently active one. Select {2} or {3} to open editors independently from the currently active one.", "Controls the default direction of editors that are opened side by side (for example, from the Explorer). By default, editors will open on the right hand side of the currently active one. If changed to `down`, the editors will open below the currently active one.", "Controls the behavior of empty editor groups when the last tab in the group is closed. When enabled, empty groups will automatically close. When disabled, empty groups will remain part of the grid.", "Controls whether an editor is revealed in any of the visible groups if opened. If disabled, an editor will prefer to open in the currently active editor group. If enabled, an already opened editor will be revealed instead of opened again in the currently active editor group. Note that there are some cases where this setting is ignored, such as when forcing an editor to open in a specific group or to the side of the currently active group.", @@ -17720,7 +19464,10 @@ "Editors are positioned from left to right.", "Controls if the centered layout should automatically resize to maximum width when more than one group is open. Once only one group is open it will resize back to the original centered width.", "Controls whether the centered layout tries to maintain constant width when the window is resized.", - "Controls whether to maximize/restore the editor group when double clicking on a tab. This value is ignored when `#workbench.editor.showTabs#` is disabled.", + "Controls how the editor group is resized when double clicking on a tab. This value is ignored when {0} is not set to {1}.", + "All other editor groups are hidden and the current editor group is maximized to take up the entire editor area.", + "The editor group takes as much space as possible by making all other editor groups as small as possible.", + "No editor group is resized when double clicking on a tab.", "Controls if the number of opened editors should be limited or not. When enabled, less recently used editors will close to make space for newly opening editors.", "Controls the maximum number of opened editors. Use the {0} setting to control this limit per editor group or across all groups.", "Controls if the maximum number of opened editors should exclude dirty editors for counting towards the configured limit.", @@ -17749,10 +19496,14 @@ "Never maximize the panel when opening it. The panel will open un-maximized.", "Open the panel to the state that it was in, before it was closed.", "Controls the visibility of the status bar at the bottom of the workbench.", - "Controls the visibility of the activity bar in the workbench.", - "Controls the behavior of clicking an activity bar icon in the workbench.", - "Hide the side bar if the clicked item is already visible.", - "Focus side bar if the clicked item is already visible.", + "Controls the location of the Activity Bar relative to the Primary and Secondary Side Bars.", + "Show the Activity Bar on the side of the Primary Side Bar and on top of the Secondary Side Bar.", + "Show the Activity Bar on top of the Primary and Secondary Side Bars.", + "Show the Activity Bar at the bottom of the Primary and Secondary Side Bars.", + "Hide the Activity Bar in the Primary and Secondary Side Bars.", + "Controls the behavior of clicking an Activity Bar icon in the workbench. This value is ignored when {0} is not set to {1}.", + "Hide the Primary Side Bar if the clicked item is already visible.", + "Focus the Primary Side Bar if the clicked item is already visible.", "Controls the visibility of view header actions. View header actions may either be always visible, or only visible when that view is focused or hovered over.", "Controls font aliasing method in the workbench.", "Sub-pixel font smoothing. On most non-retina displays this will give the sharpest text.", @@ -17768,7 +19519,7 @@ "Do not render with reduced motion", "Render with reduced motion based on OS configuration.", "Controls whether the layout control in the title bar is shown.", - "Controls whether the layout control is shown in the custom title bar. This setting only has an effect when {0} is set to {1}.", + "Controls whether the layout control is shown in the custom title bar. This setting only has an effect when {0} is not set to {1}.", "Shows a single button with a dropdown of layout options.", "Shows several buttons for toggling the visibility of the panels and side bar.", "Shows both the dropdown and toggle buttons.", @@ -17791,11 +19542,13 @@ "`${remoteName}`: e.g. SSH", "`${dirty}`: an indicator for when the active editor has unsaved changes.", "`${focusedView}`: the name of the view that is currently focused.", + "`${activeRepositoryName}`: the name of the active repository (e.g. vscode).", + "`${activeRepositoryBranchName}`: the name of the active branch in the active repository (e.g. main).", "`${separator}`: a conditional separator (\" - \") that only shows when surrounded by variables with values or static text.", "Window", "Separator used by {0}.", "Show command launcher together with the window title.", - "Show command launcher together with the window title. This setting only has an effect when {0} is set to {1}.", + "Show command launcher together with the window title. This setting only has an effect when {0} is not set to {1}.", "Menu is displayed at the top of the window and only hidden in full screen mode.", "Menu is always visible at the top of the window even in full screen mode.", "Menu is hidden but can be displayed at the top of the window by executing the `Focus Application Menu` command.", @@ -17824,11 +19577,15 @@ "Never explicitly ask for confirmation unless data loss is imminent.", "Never explicitly ask for confirmation.", "Controls whether to show a confirmation dialog before closing the browser tab or window. Note that even if enabled, browsers may still decide to close a tab or window without confirmation and that this setting is only a hint that may not work in all cases.", - "Controls whether to show a confirmation dialog before closing the window or quitting the application.", + "Controls whether to show a confirmation dialog before closing a window or quitting the application.", + "Controls whether the problems are visible throughout the editor and workbench.", "Zen Mode", "Controls whether turning on Zen Mode also puts the workbench into full screen mode.", "Controls whether turning on Zen Mode also centers the layout.", - "Controls whether turning on Zen Mode also hides workbench tabs.", + "Controls whether turning on Zen Mode should show multiple editor tabs, a single editor tab, or hide the editor title area completely.", + "Each editor is displayed as a tab in the editor title area.", + "The active editor is displayed as a single large tab in the editor title area.", + "The editor title area is not displayed.", "Controls whether turning on Zen Mode also hides the status bar at the bottom of the workbench.", "Controls whether turning on Zen Mode also hides the activity bar either at the left or right of the workbench.", "Controls whether turning on Zen Mode also hides the editor line numbers.", @@ -17844,13 +19601,8 @@ "Select All" ], "vs/workbench/browser/actions/developerActions": [ - "Inspect Context Keys", - "Toggle Screencast Mode", - "Log Storage Database Contents", "The storage database contents have been logged to the developer tools.", "Open developer tools from the menu and select the Console tab.", - "Log Working Copies", - "Remove Large Storage Database Entries...", "Scope: {0}, Target: {1}", "Global", "Profile", @@ -17863,9 +19615,6 @@ "Do you want to remove the selected storage entries from the database?", "{0}\n\nThis action is irreversible and may result in data loss!", "&&Remove", - "Start Tracking Disposables", - "Snapshot Tracked Disposables", - "Stop Tracking Disposables", "Screencast Mode", "Controls the vertical offset of the screencast mode overlay from the bottom as a percentage of the workbench height.", "Controls the font size (in pixels) of the screencast mode keyboard.", @@ -17877,26 +19626,34 @@ "Show single editor cursor move commands.", "Controls how long (in milliseconds) the keyboard overlay is shown in screencast mode.", "Controls the color in hex (#RGB, #RGBA, #RRGGBB or #RRGGBBAA) of the mouse indicator in screencast mode.", - "Controls the size (in pixels) of the mouse indicator in screencast mode." + "Controls the size (in pixels) of the mouse indicator in screencast mode.", + "Inspect Context Keys", + "Toggle Screencast Mode", + "Log Storage Database Contents", + "Log Working Copies", + "Remove Large Storage Database Entries...", + "Start Tracking Disposables", + "Snapshot Tracked Disposables", + "Stop Tracking Disposables" ], "vs/workbench/browser/actions/helpActions": [ - "Keyboard Shortcuts Reference", "&&Keyboard Shortcuts Reference", - "Video Tutorials", "&&Video Tutorials", - "Tips and Tricks", "Tips and Tri&&cks", - "Documentation", "&&Documentation", + "&&Join Us on YouTube", + "&&Search Feature Requests", + "View &&License", + "Privac&&y Statement", + "Keyboard Shortcuts Reference", + "Video Tutorials", + "Tips and Tricks", + "Documentation", "Signup for the VS Code Newsletter", "Join Us on YouTube", - "&&Join Us on YouTube", "Search Feature Requests", - "&&Search Feature Requests", "View License", - "View &&License", - "Privacy Statement", - "Privac&&y Statement" + "Privacy Statement" ], "vs/workbench/browser/actions/layoutActions": [ "Represents the menu bar", @@ -17915,17 +19672,10 @@ "Represents full screen", "Represents centered layout mode", "Represents zen mode", - "Close Primary Side Bar", - "Toggle Activity Bar Visibility", - "&&Activity Bar", - "Toggle Centered Layout", "&&Centered Layout", - "Move Primary Side Bar Right", - "Move Primary Side Bar Left", "Toggle Primary Side Bar Position", "Move Primary Side Bar Right", "Move Primary Side Bar Left", - "Toggle Primary Side Bar Position", "Icon represents workbench layout configuration.", "Configure Layout", "Move Primary Side Bar Right", @@ -17936,32 +19686,25 @@ "Move Secondary Side Bar Right", "&&Move Primary Side Bar Right", "&&Move Primary Side Bar Left", - "Toggle Editor Area Visibility", "Show &&Editor Area", "&&Appearance", - "Toggle Primary Side Bar Visibility", "Primary Side Bar", "&&Primary Side Bar", "Hide Primary Side Bar", "Hide Primary Side Bar", "Toggle Primary Side Bar", "Toggle Primary Side Bar", - "Toggle Status Bar Visibility", "S&&tatus Bar", - "Toggle Editor Tab Visibility", - "Separate Pinned Editor Tabs", - "Toggle Zen Mode", + "Tab Bar", + "Tab Bar", + "Editor Actions Position", "Zen Mode", - "Toggle Menu Bar", "Menu &&Bar", "Menu Bar", - "Reset View Locations", - "Move View", "Side Bar / {0}", "Panel / {0}", "Secondary Side Bar / {0}", "Select a View to Move", - "Move Focused View", "There is no view currently focused.", "The currently focused view is not movable.", "Select a Destination for the View", @@ -17972,14 +19715,7 @@ "Side Bar", "Panel", "Secondary Side Bar", - "Reset Focused View Location", "There is no view currently focused.", - "Increase Current View Size", - "Increase Editor Width", - "Increase Editor Height", - "Decrease Current View Size", - "Decrease Editor Width", - "Decrease Editor Height", "Select to Hide", "Select to Show", "Active", @@ -17998,14 +19734,56 @@ "Full Screen", "Zen Mode", "Centered Layout", - "Customize Layout...", "Visibility", "Primary Side Bar Position", "Panel Alignment", "Modes", "Customize Layout", "Close", - "Restore Defaults" + "Restore Defaults", + "Close Primary Side Bar", + "Toggle Centered Layout", + "Move Primary Side Bar Right", + "Move Primary Side Bar Left", + "Toggle Primary Side Bar Position", + "Toggle Editor Area Visibility", + "Toggle Primary Side Bar Visibility", + "Toggle Status Bar Visibility", + "Hide Editor Tabs", + "Hide Tab Bar", + "Hide Editor Tabs in Zen Mode", + "Hide Tab Bar in Zen Mode", + "Show Multiple Editor Tabs", + "Show Tab Bar with multiple tabs", + "Show Multiple Editor Tabs in Zen Mode", + "Show Tab Bar in Zen Mode", + "Show Single Editor Tab", + "Show Tab Bar with one Tab", + "Show Single Editor Tab in Zen Mode", + "Show Tab Bar in Zen Mode with one Tab", + "Move Editor Actions to Title Bar", + "Move Editor Actions from the tab bar to the title bar", + "Move Editor Actions to Tab Bar", + "Move Editor Actions from the title bar to the tab bar", + "Hide Editor Actions", + "Hide Editor Actions in the tab and title bar", + "Show Editor Actions", + "Make Editor Actions visible.", + "Separate Pinned Editor Tabs", + "Toggle whether pinned editor tabs are shown on a separate row above unpinned tabs.", + "Toggle Zen Mode", + "Toggle Menu Bar", + "Reset View Locations", + "Move View", + "Move Focused View", + "Reset Focused View Location", + "Increase Current View Size", + "Increase Editor Width", + "Increase Editor Height", + "Decrease Current View Size", + "Decrease Editor Width", + "Decrease Editor Height", + "Customize Layout..." ], "vs/workbench/browser/actions/navigationActions": [ "Navigate to the View on the Left", @@ -18015,6 +19793,11 @@ "Focus Next Part", "Focus Previous Part" ], + "vs/workbench/browser/actions/listCommands": [ + "&&Toggle Tree Sticky Scroll", + "Toggles Sticky Scroll widget at the top of tree structures such as the File Explorer and Debug variables View.", + "Toggle Tree Sticky Scroll" + ], "vs/workbench/browser/actions/windowActions": [ "Remove from Recently Opened", "Folder With Unsaved Files", @@ -18032,21 +19815,37 @@ "Folders with unsaved files cannot be removed until all unsaved files have been saved or reverted.", "{0}, workspace with unsaved changes", "{0}, folder with unsaved changes", - "Open Recent...", "&&More...", + "&&Full Screen", + "&&About", + "New &&Window", + "Confirm Before Close", + "Open &&Recent", + "Open Recent...", "Quick Open Recent...", "Toggle Full Screen", - "&&Full Screen", "Reload Window", "About", - "&&About", "New Window", - "New &&Window", - "Remove keyboard focus from focused element", - "Confirm Before Close", - "Open &&Recent" + "Remove keyboard focus from focused element" + ], + "vs/workbench/browser/actions/workspaceCommands": [ + "&&Add", + "Add Folder to Workspace", + "Select workspace folder", + "Add Folder to Workspace..." ], "vs/workbench/browser/actions/workspaceActions": [ + "&&Open File...", + "Open &&Folder...", + "Open &&Folder...", + "&&Open...", + "Open Wor&&kspace from File...", + "A&&dd Folder to Workspace...", + "Save Workspace As...", + "Duplicate Workspace", + "Close &&Folder", + "Close &&Workspace", "Workspaces", "Open File...", "Open Folder...", @@ -18057,27 +19856,11 @@ "Open Workspace Configuration File", "Remove Folder from Workspace...", "Save Workspace As...", - "Duplicate As Workspace in New Window", - "&&Open File...", - "Open &&Folder...", - "Open &&Folder...", - "&&Open...", - "Open Wor&&kspace from File...", - "A&&dd Folder to Workspace...", - "Save Workspace As...", - "Duplicate Workspace", - "Close &&Folder", - "Close &&Workspace" - ], - "vs/workbench/browser/actions/workspaceCommands": [ - "Add Folder to Workspace...", - "&&Add", - "Add Folder to Workspace", - "Select workspace folder" + "Duplicate As Workspace in New Window" ], "vs/workbench/browser/actions/quickAccessActions": [ - "Go to File...", "Quick Open", + "Go to File...", "Navigate Next in Quick Open", "Navigate Previous in Quick Open", "Select Next in Quick Open", @@ -18098,14 +19881,25 @@ "The debug callstack view context menu", "The debug variables view context menu", "The debug toolbar menu", + "The notebook variables view context menu", "The home indicator context menu (web only)", "'Copy as' submenu in the top level Edit menu", "The Source Control title menu", "The Source Control menu", + "The Source Control title menu", "The Source Control resource state context menu", "The Source Control resource folder context menu", "The Source Control resource group context menu", "The Source Control inline change menu", + "The Source Control input box menu", + "The Source Control incoming changes menu", + "The Source Control incoming changes context menu", + "The Source Control outgoing changes menu", + "The Source Control outgoing changes context menu", + "The Source Control all incoming changes context menu", + "The Source Control incoming changes history item context menu", + "The Source Control all outgoing changes context menu", + "The Source Control outgoing changes history item context menu", "The remote indicator menu in the status bar", "The terminal context menu", "The terminal tabs context menu", @@ -18119,14 +19913,17 @@ "The contributed comment title menu", "The contributed comment context menu, rendered as buttons below the comment editor", "The contributed comment context menu, rendered as a right click menu on the an individual comment in the comment thread's peek view.", + "The contributed comment thread context menu in the comments view", "The contributed notebook toolbar menu", "The contributed notebook kernel sources menu", "The contributed notebook cell title menu", "The contributed notebook cell execution menu", "The contributed interactive toolbar menu", "The contributed interactive cell title menu", + "The contributed issue reporter menu", "The contributed test item menu", "The menu for a gutter decoration for a test item", + "The menu for an item in the Test Results view or peek.", "A prominent button overlaying editor content where the message is displayed", "Context menu for the message in the results tree", "The extension context menu", @@ -18139,9 +19936,13 @@ "The webview context menu", "Share submenu shown in the top level File menu.", "The actions shown when hovering on an inline completion", + "The actions shown when hovering on an inline edit", "The prominent button in an editor, overlays its content", "The contributed editor line number context menu", "The result toolbar of the merge editor", + "The resource toolbar in the multi diff editor", + "The gutter toolbar in the diff editor", + "The gutter toolbar in the diff editor", "property `{0}` is mandatory and must be of type `string`", "property `{0}` can be omitted or must be of type `string`", "property `{0}` can be omitted or must be of type `string`", @@ -18195,7 +19996,12 @@ "Menu item references the same command as default and alt-command", "Menu item references a submenu for a menu which doesn't have submenu support.", "Menu item references a submenu `{0}` which is not defined in the 'submenus' section.", - "The `{0}` submenu was already contributed to the `{1}` menu." + "The `{0}` submenu was already contributed to the `{1}` menu.", + "ID", + "Title", + "Keyboard Shortcuts", + "Menu Contexts", + "Commands" ], "vs/workbench/api/common/configurationExtensionPoint": [ "A title for the current category of settings. This label will be rendered in the Settings editor as a subheading. If the title is the same as the extension display name, then the category will be grouped under the main extension heading.", @@ -18239,7 +20045,11 @@ "Workspace extensions", "The remote server where the workspace is located.", "A transient workspace will disappear when restarting or reloading.", - "Unknown workspace configuration property" + "Unknown workspace configuration property", + "ID", + "Description", + "Default", + "Settings" ], "vs/workbench/api/browser/viewsExtensionPoint": [ "Unique id used to identify the container in which views can be contributed using 'views' contribution point", @@ -18261,6 +20071,7 @@ "The view will not be shown in the view container, but will be discoverable through the views menu and other view entry points and can be un-hidden by the user.", "The view will show in the view container, but will be collapsed.", "The initial size of the view. The size will behave like the css 'flex' property, and will set the initial size when the view is first shown. In the side bar, this is the height of the view. This value is only respected when the same extension owns both the view and the view container.", + "When the accessibility help dialog is invoked in this view, this content will be presented to the user as a markdown string. Keybindings will be resolved when provided in the format of . If there is no keybinding, that will be indicated with a link to configure one.", "Identifier of the view. This should be unique across all views. It is recommended to include your extension id as part of the view id. Use this to register a data provider through `vscode.window.registerTreeDataProviderForView` API. Also to trigger activating your extension by registering `onView:${id}` event to `activationEvents`.", "The human-readable name of the view. Will be shown", "Condition which must be true to show this view", @@ -18290,7 +20101,15 @@ "property `{0}` can be omitted or must be of type `string`", "property `{0}` can be omitted or must be of type `string`", "property `{0}` can be omitted or must be of type `string`", - "property `{0}` can be omitted or must be one of {1}" + "property `{0}` can be omitted or must be one of {1}", + "ID", + "Title", + "Where", + "ID", + "Name", + "Where", + "View Containers", + "Views" ], "vs/workbench/browser/parts/editor/editor.contribution": [ "Text Editor", @@ -18303,20 +20122,34 @@ "Show All Opened Editors By Appearance", "Type the name of an editor to open it.", "Show All Opened Editors By Most Recently Used", + "Lock Group", "Unlock Group", "Close Group", "Split Up", "Split Down", "Split Left", "Split Right", + "New Window", "Lock Group", "Close", "Split Up", "Split Down", "Split Left", "Split Right", - "Editor Tabs", - "Separate Pinned Editor Tabs", + "Move into New Window", + "Copy into New Window", + "Tab Bar", + "Multiple Tabs", + "Single Tab", + "Hidden", + "Tab Bar", + "Multiple Tabs", + "Single Tab", + "Hidden", + "Editor Actions Position", + "Tab Bar", + "Title Bar", + "Hidden", "Close", "Close Others", "Close to the Right", @@ -18332,11 +20165,15 @@ "Split Right", "Split in Group", "Join in Group", + "Move into New Window", + "Copy into New Window", "Inline View", "Show Opened Editors", "Close All", "Close Saved", "Enable Preview Editors", + "Maximize Group", + "Unmaximize Group", "Lock Group", "Split Editor Right", "Split Editor Down", @@ -18351,55 +20188,34 @@ "Close", "Unpin", "Close", + "Lock Group", "Unlock Group", "Icon for the previous change action in the diff editor.", - "Icon for the next change action in the diff editor.", - "Icon for the toggle whitespace action in the diff editor.", "Previous Change", + "Icon for the next change action in the diff editor.", "Next Change", + "Swap Left and Right Side", + "Icon for the toggle whitespace action in the diff editor.", "Show Leading/Trailing Whitespace Differences", - "Keep Editor", - "Pin Editor", - "Unpin Editor", - "Close Editor", - "Close Pinned Editor", - "Close All Editors in Group", - "Close Saved Editors in Group", - "Close Other Editors in Group", - "Close Editors to the Right in Group", - "Close Editor Group", - "Reopen Editor With...", "&&Reopen Closed Editor", - "&&Clear Recently Opened", + "&&Clear Recently Opened...", "Share", "Editor &&Layout", - "Split Up", "Split &&Up", - "Split Down", "Split &&Down", - "Split Left", "Split &&Left", - "Split Right", "Split &&Right", - "Split in Group", "Split in &&Group", - "Join in Group", "Join in &&Group", - "Single", + "&&Move Editor into New Window", + "&&Copy Editor into New Window", "&&Single", - "Two Columns", "&&Two Columns", - "Three Columns", "T&&hree Columns", - "Two Rows", "T&&wo Rows", - "Three Rows", "Three &&Rows", - "Grid (2x2)", "&&Grid (2x2)", - "Two Rows Right", "Two R&&ows Right", - "Two Columns Bottom", "Two &&Columns Bottom", "&&Last Edit Location", "&&First Side in Editor", @@ -18424,21 +20240,43 @@ "Group &&Right", "Group &&Above", "Group &&Below", - "Switch &&Group" - ], - "vs/workbench/browser/parts/statusbar/statusbarPart": [ - "Hide Status Bar" + "Switch &&Group", + "Keep Editor", + "Pin Editor", + "Unpin Editor", + "Close Editor", + "Close Pinned Editor", + "Close All Editors in Group", + "Close Saved Editors in Group", + "Close Other Editors in Group", + "Close Editors to the Right in Group", + "Close Editor Group", + "Reopen Editor With...", + "Split Up", + "Split Down", + "Split Left", + "Split Right", + "Split in Group", + "Join in Group", + "Move Editor into New Window", + "Copy Editor into New Window", + "Single", + "Two Columns", + "Three Columns", + "Two Rows", + "Three Rows", + "Grid (2x2)", + "Two Rows Right", + "Two Columns Bottom" ], "vs/workbench/browser/parts/banner/bannerPart": [ "Focus Banner" ], - "vs/workbench/browser/parts/views/viewsService": [ - "Show {0}", - "Toggle {0}", - "Show {0}", - "Toggle {0}", - "Focus on {0} View", - "Reset Location" + "vs/workbench/browser/parts/editor/editorParts": [ + "Window {0}" + ], + "vs/workbench/browser/parts/statusbar/statusbarPart": [ + "Hide Status Bar" ], "vs/platform/undoRedo/common/undoRedoService": [ "The following files have been closed and modified on disk: {0}.", @@ -18464,19 +20302,15 @@ ], "vs/workbench/services/extensions/browser/extensionUrlHandler": [ "Allow '{0}' extension to open this URI?", - "Don't ask again for this extension.", + "Do not ask me again for this extension", "&&Open", - "Would you like to install '{0}' extension from '{1}' to open this URI?", - "'{0}' extension wants to open a URI:", - "&&Install and Open", - "Installing Extension '{0}'...", - "Extension '{0}' is disabled. Would you like to enable the extension and open the URL?", - "&&Enable and Open", + "This extension wants to open a URI:", + "Open URI", "Extension '{0}' is not loaded. Would you like to reload the window to load the extension and open the URL?", "&&Reload Window and Open", + "There are currently no authorized extension URIs.", "Manage Authorized Extension URIs...", - "Extensions", - "There are currently no authorized extension URIs." + "Extensions" ], "vs/workbench/services/keybinding/common/keybindingEditing": [ "Unable to write because the keybindings configuration file has unsaved changes. Please save it first and then try again.", @@ -18568,6 +20402,12 @@ "A icon to use as file icon, if no icon theme provides one for the language.", "Icon path when a light theme is used", "Icon path when a dark theme is used", + "ID", + "Name", + "File Extensions", + "Grammar", + "Snippets", + "Programming Languages", "Invalid `contributes.{0}`. Expected an array.", "Empty value for `contributes.{0}`", "property `{0}` is mandatory and must be of type `string`", @@ -18618,20 +20458,16 @@ "Workspace Folder", "Workspace" ], + "vs/workbench/services/extensionManagement/common/extensionFeaturesManagemetService": [ + "Access '{0}' Feature", + "'{0}' extension would like to access the '{1}' feature.", + "Allow", + "Don't Allow" + ], "vs/workbench/services/notification/common/notificationService": [ "Don't Show Again", "Don't Show Again" ], - "vs/workbench/services/userDataProfile/browser/userDataProfileManagement": [ - "The current profile has been updated. Please reload to switch back to the updated profile", - "The current profile has been removed. Please reload to switch back to default profile", - "The current profile has been removed. Please reload to switch back to default profile", - "Cannot rename the default profile", - "Cannot delete the default profile", - "Switching to a profile.", - "Switching a profile requires reloading VS Code.", - "&&Reload" - ], "vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService": [ "Error while importing profile: {0}", "{0}: Resolving profile content...", @@ -18713,6 +20549,26 @@ "Export Profile", "Profile name must be provided." ], + "vs/workbench/services/userDataProfile/browser/userDataProfileManagement": [ + "The current profile has been updated. Please reload to switch back to the updated profile", + "The current profile has been removed. Please reload to switch back to default profile", + "The current profile has been removed. Please reload to switch back to default profile", + "Cannot rename the default profile", + "Cannot delete the default profile", + "Switching to a profile.", + "Switching a profile requires reloading VS Code.", + "&&Reload" + ], + "vs/workbench/services/remote/common/remoteExplorerService": [ + "The ID of a Get Started walkthrough to open.", + "Contributes help information for Remote", + "The url, or a command that returns the url, to your project's Getting Started page, or a walkthrough ID contributed by your project's extension", + "The url, or a command that returns the url, to your project's documentation page", + "The url, or a command that returns the url, to your project's feedback reporter", + "Use {0} instead", + "The url, or a command that returns the url, to your project's issue reporter", + "The url, or a command that returns the url, to your project's issues list" + ], "vs/workbench/services/filesConfiguration/common/filesConfigurationService": [ "Editor is read-only because the file system of the file is read-only.", "Editor is read-only because the file was set read-only in this session. [Click here](command:{0}) to set writeable.", @@ -18724,6 +20580,15 @@ "Hide '{0}'", "Reset Location" ], + "vs/workbench/services/views/browser/viewsService": [ + "Text Editor", + "Show {0}", + "Toggle {0}", + "Show {0}", + "Toggle {0}", + "Focus on {0} View", + "Reset Location" + ], "vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService": [ "Settings sync cannot be turned on because there are no authentication providers available.", "No account available", @@ -18752,14 +20617,11 @@ "Sign in with {0}" ], "vs/workbench/services/authentication/browser/authenticationService": [ - "The id of the authentication provider.", - "The human readable name of the authentication provider.", - "Contributes authentication", - "No accounts requested yet...", "An authentication contribution must specify an id.", "An authentication contribution must specify a label.", - "This authentication id '{0}' has already been registered", - "Loading...", + "This authentication id '{0}' has already been registered" + ], + "vs/workbench/services/authentication/browser/authenticationExtensionsService": [ "Sign in requested", "The extension '{0}' wants to access the {1} account '{2}'.", "&&Allow", @@ -18773,82 +20635,83 @@ "vs/workbench/services/assignment/common/assignmentService": [ "Fetches experiments to run from a Microsoft online service." ], + "vs/workbench/services/issue/browser/issueTroubleshoot": [ + "Troubleshoot Issue", + "Issue troubleshooting is a process to help you identify the cause for an issue. The cause for an issue can be a misconfiguration, due to an extension, or be {0} itself.\n\nDuring the process the window reloads repeatedly. Each time you must confirm if you are still seeing the issue.", + "&&Troubleshoot Issue", + "Issue troubleshooting is active and has temporarily disabled all installed extensions. Check if you can still reproduce the problem and proceed by selecting from these options.", + "Issue troubleshooting is active and has temporarily reset your configurations to defaults. Check if you can still reproduce the problem and proceed by selecting from these options.", + "Issue troubleshooting has identified that the issue is caused by your configurations. Please report the issue by exporting your configurations using \"Export Profile\" command and share the file in the issue report.", + "Issue troubleshooting has identified that the issue is with {0}.", + "I Can't Reproduce", + "I Can Reproduce", + "Stop", + "Troubleshoot Issue", + "This likely means that the issue has been addressed already and will be available in an upcoming release. You can safely use {0} insiders until the new stable version is available.", + "Troubleshoot Issue", + "Download {0} Insiders", + "Report Issue Anyway", + "Please try to download and reproduce the issue in {0} insiders.", + "Troubleshoot Issue", + "I can't reproduce", + "I can reproduce", + "Stop", + "Please try to reproduce the issue in {0} insiders and confirm if the issue exists there.", + "Troubleshoot Issue...", + "Stop Troubleshoot Issue" + ], + "vs/workbench/contrib/preferences/browser/keybindingsEditorContribution": [ + "You won't be able to produce this key combination under your current keyboard layout.", + "**{0}** for your current keyboard layout (**{1}** for US standard).", + "**{0}** for your current keyboard layout." + ], "vs/workbench/contrib/preferences/browser/preferences.contribution": [ "Settings Editor 2", "Keybindings Editor", - "Open Settings (UI)", - "Open User Settings (JSON)", - "Open Application Settings (JSON)", - "Preferences", - "Settings", "&&Settings", - "Open Settings (UI)", - "Open User Settings", - "Open Default Settings (JSON)", - "Open Workspace Settings", - "Open Accessibility Settings", + "Open Folder Settings", + "&&Online Services Settings", + "&&Telemetry Settings", + "Focus settings file", + "Focus settings file", + "Focus settings list", + "Focus Setting Control", + "Keyboard Shortcuts", + "Keyboard Shortcuts", + "Clear Search Results", + "Clear Keyboard Shortcuts Search History", + "&&Preferences", + "Open Settings (UI)", + "Open User Settings (JSON)", + "Open Application Settings (JSON)", + "Preferences", + "Settings", + "Open Settings (UI)", + "Opens the JSON file containing the current user profile settings", + "Open User Settings", + "Open Default Settings (JSON)", + "Open Workspace Settings", + "Open Accessibility Settings", "Open Workspace Settings (JSON)", "Open Folder Settings", "Open Folder Settings (JSON)", - "Open Folder Settings", - "&&Online Services Settings", "Show untrusted workspace settings", - "&&Telemetry Settings", "Open Remote Settings ({0})", "Open Remote Settings (JSON) ({0})", "Focus Settings Search", "Clear Settings Search Results", - "Focus settings file", - "Focus settings file", - "Focus settings list", "Focus Settings Table of Contents", - "Focus Setting Control", "Show Setting Context Menu", "Move Focus Up One Level", "Preferences", "Open Keyboard Shortcuts", - "Keyboard Shortcuts", - "Keyboard Shortcuts", "Open Default Keyboard Shortcuts (JSON)", "Open Keyboard Shortcuts (JSON)", "Show System Keybindings", "Show Extension Keybindings", "Show User Keybindings", - "Clear Search Results", - "Clear Keyboard Shortcuts Search History", "Define Keybinding", - "Open Settings (JSON)", - "&&Preferences" - ], - "vs/workbench/services/issue/browser/issueTroubleshoot": [ - "Troubleshoot Issue", - "Issue troubleshooting is a process to help you identify the cause for an issue. The cause for an issue can be a misconfiguration, due to an extension, or be {0} itself.\n\nDuring the process the window reloads repeatedly. Each time you must confirm if you are still seeing the issue.", - "&&Troubleshoot Issue", - "Issue troubleshooting is active and has temporarily disabled all installed extensions. Check if you can still reproduce the problem and proceed by selecting from these options.", - "Issue troubleshooting is active and has temporarily reset your configurations to defaults. Check if you can still reproduce the problem and proceed by selecting from these options.", - "Issue troubleshooting has identified that the issue is caused by your configurations. Please report the issue by exporting your configurations using \"Export Profile\" command and share the file in the issue report.", - "Issue troubleshooting has identified that the issue is with {0}.", - "I Can't Reproduce", - "I Can Reproduce", - "Stop", - "Troubleshoot Issue", - "This likely means that the issue has been addressed already and will be available in an upcoming release. You can safely use {0} insiders until the new stable version is available.", - "Troubleshoot Issue", - "Download {0} Insiders", - "Report Issue Anyway", - "Please try to download and reproduce the issue in {0} insiders.", - "Troubleshoot Issue", - "I can't reproduce", - "I can reproduce", - "Stop", - "Please try to reproduce the issue in {0} insiders and confirm if the issue exists there.", - "Troubleshoot Issue...", - "Stop Troubleshoot Issue" - ], - "vs/workbench/contrib/preferences/browser/keybindingsEditorContribution": [ - "You won't be able to produce this key combination under your current keyboard layout.", - "**{0}** for your current keyboard layout (**{1}** for US standard).", - "**{0}** for your current keyboard layout." + "Open Settings (JSON)" ], "vs/workbench/contrib/performance/browser/performance.contribution": [ "Startup Performance", @@ -18856,6 +20719,19 @@ "Print Service Traces", "Print Emitter Profiles" ], + "vs/workbench/contrib/chat/browser/chat.contribution": [ + "Chat", + "Controls the font size in pixels in chat codeblocks.", + "Controls the font family in chat codeblocks.", + "Controls the font weight in chat codeblocks.", + "Controls whether lines should wrap in chat codeblocks.", + "Controls the line height in pixels in chat codeblocks. Use 0 to compute the line height from the font size.", + "Controls whether a checkbox is shown to allow the user to determine which implicit context is included with a chat participant's prompt.", + "Chat", + "Chat", + "Start a new chat", + "Choose a file in the workspace" + ], "vs/workbench/contrib/notebook/browser/notebook.contribution": [ "Settings for code editors used in notebooks. This can be used to customize most editor.* settings.", "Notebook", @@ -18879,6 +20755,9 @@ "The insert actions don't appear anywhere.", "Control whether to render a global toolbar inside the notebook editor.", "Experimental. Control whether to render notebook Sticky Scroll headers in the notebook editor.", + "Control whether nested sticky lines appear to stack flat or indented.", + "Nested sticky lines appear flat.", + "Nested sticky lines appear indented.", "Control whether outputs action should be rendered in the output toolbar.", "Controls when the Markdown header folding arrow is shown.", "The folding controls are always visible.", @@ -18888,15 +20767,18 @@ "Control whether extra actions are shown in a dropdown next to the run button.", "Control whether the actions on the notebook toolbar should render label or not.", "Controls how many lines of text are displayed in a text output. If {0} is enabled, this setting is used to determine the scroll height of the output.", + "Control whether to disable filepath links in the output of notebook cells.", + "Control whether to render error output in a minimal style.", "Controls the font size in pixels of rendered markup in notebooks. When set to {0}, 120% of {1} is used.", "Controls whether code cells in the interactive window are collapsed by default.", "Line height of the output text within notebook cells.\n - When set to 0, editor line height is used.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values.", "Font size for the output text within notebook cells. When set to 0, {0} is used.", "The font family of the output text within notebook cells. When set to empty, the {0} is used.", - "Initially render notebook outputs in a scrollable region when longer than the limit", + "Initially render notebook outputs in a scrollable region when longer than the limit.", "Controls whether the lines in output should wrap.", "Format a notebook on save. A formatter must be available, the file must not be saved after delay, and the editor must not be shutting down.", - "Run a series of CodeActions for a notebook on save. CodeActions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `\"notebook.source.organizeImports\": \"explicit\"`", + "When enabled, insert a final new line into the end of code cells when saving a notebook.", + "Run a series of Code Actions for a notebook on save. Code Actions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `\"notebook.source.organizeImports\": \"explicit\"`", "Triggers Code Actions only when explicitly saved.", "Never triggers Code Actions on save.", "Triggers Code Actions only when explicitly saved. This value will be deprecated in favor of \"explicit\".", @@ -18909,51 +20791,28 @@ "Scroll to fully reveal the next cell.", "Scroll to reveal the first line of the next cell.", "Do not scroll.", - "Experimental. Keep the focused cell steady while surrounding cells change size.", - "Anchor the viewport to the focused cell depending on context unless {0} is set to {1}.", - "Always anchor the viewport to the focused cell.", - "The focused cell may shift around as cells resize." - ], - "vs/workbench/contrib/chat/browser/chat.contribution": [ - "Chat", - "Controls the font size in pixels in chat codeblocks.", - "Controls the font family in chat codeblocks.", - "Controls the font weight in chat codeblocks.", - "Controls whether lines should wrap in chat codeblocks.", - "Controls the line height in pixels in chat codeblocks. Use 0 to compute the line height from the font size.", - "Chat", - "Chat", - "Clear the session" - ], - "vs/workbench/contrib/testing/browser/testing.contribution": [ - "Testing", - "T&&esting", - "Test Results", - "Test Results", - "No tests have been found in this workspace yet.", - "Install Additional Test Extensions...", - "Test Explorer" - ], - "vs/workbench/contrib/logs/common/logs.contribution": [ - "Set Default Log Level", - "{0} (Remote)", - "Show Window Log" + "Enable experimental floating chat widget in notebooks.", + "Enable experimental generate action to create code cell with inline chat enabled.", + "Enable the experimental notebook variables view within the debug panel.", + "Show available diagnostics for cell failures.", + "The limit of notebook output size in kilobytes (KB) where notebook files will no longer be backed up for hot reload. Use 0 for unlimited." ], "vs/workbench/contrib/interactive/browser/interactive.contribution": [ - "Interactive Window", "Open Interactive Window", + "Scroll to Top", + "Scroll to Bottom", + "The border color for the current interactive code cell when the editor has focus.", + "The border color for the current interactive code cell when the editor does not have focus.", + "Automatically scroll the interactive window to show the output of the last statement executed. If this value is false, the window will only scroll if the last cell was already the one scrolled to.", + "Prompt to save the interactive window when it is closed. Only new interactive windows will be affected by this setting change.", + "Interactive Window", "Open Interactive Window", "Execute Code", "Clear the interactive window input editor contents", "Previous value in history", "Next value in history", - "Scroll to Top", - "Scroll to Bottom", "Focus Input Editor", - "Focus History", - "The border color for the current interactive code cell when the editor has focus.", - "The border color for the current interactive code cell when the editor does not have focus.", - "Automatically scroll the interactive window to show the output of the last statement executed. If this value is false, the window will only scroll if the last cell was already the one scrolled to." + "Focus History" ], "vs/workbench/contrib/quickaccess/browser/quickAccess.contribution": [ "Type '{0}' to get help on the actions you can take from here.", @@ -18970,18 +20829,43 @@ "Command Palette...", "Command Palette..." ], + "vs/workbench/contrib/testing/browser/testing.contribution": [ + "T&&esting", + "No tests have been found in this workspace yet.", + "Install Additional Test Extensions...", + "Testing", + "Test Results", + "Test Results", + "Test Explorer", + "Test Coverage" + ], + "vs/workbench/contrib/files/browser/explorerViewlet": [ + "View icon of the explorer view.", + "View icon of the open editors view.", + "&&Explorer", + "Open Folder", + "add a folder", + "Open Recent", + "You have not yet added a folder to the workspace.\n{0}", + "You have not yet opened a folder.\n{0}\n{1}", + "Connected to remote.\n{0}", + "You have not yet opened a folder.\n{0}\nOpening a folder will close all currently open editors. To keep them open, {1} instead.", + "You have not yet opened a folder.\n{0}", + "Folders", + "Explorer", + "Explorer" + ], + "vs/workbench/contrib/logs/common/logs.contribution": [ + "{0} (Remote)", + "Set Default Log Level", + "Show Window Log" + ], "vs/workbench/contrib/files/browser/fileActions.contribution": [ "Copy Path", "Copy Relative Path", "Reveal in Explorer View", "Use your changes and overwrite file contents", "Discard your changes and revert to file contents", - "Copy Path of Active File", - "Copy Relative Path of Active File", - "Save All in Group", - "Save All Files", - "Revert File", - "Compare Active File with Saved", "Open to the Side", "Reopen Editor With...", "Revert File", @@ -19007,23 +20891,35 @@ "A&&uto Save", "Re&&vert File", "&&Close Editor", - "Go to &&File..." + "Go to &&File...", + "Copy Path of Active File", + "Copy Relative Path of Active File", + "Save All in Group", + "Save All Files", + "Revert File", + "Compare Active File with Saved", + "Opens a new diff editor to compare the active file with the version on disk.", + "Create a new folder or directory" ], - "vs/workbench/contrib/files/browser/explorerViewlet": [ - "View icon of the explorer view.", - "View icon of the open editors view.", - "Folders", - "Explorer", - "Explorer", - "&&Explorer", - "Open Folder", - "add a folder", - "Open Recent", - "You have not yet added a folder to the workspace.\n{0}", - "You have not yet opened a folder.\n{0}\n{1}", - "Connected to remote.\n{0}", - "You have not yet opened a folder.\n{0}\nOpening a folder will close all currently open editors. To keep them open, {1} instead.", - "You have not yet opened a folder.\n{0}" + "vs/workbench/contrib/bulkEdit/browser/bulkEditService": [ + "Made no edits", + "Made {0} text edits in {1} files", + "Made {0} text edits in one file", + "Made {0} text edits in {1} files, also created or deleted {2} files", + "Workspace Edit", + "Workspace Edit", + "Made no edits", + "Are you sure you want to close the window?", + "&&Close Window", + "Are you sure you want to change the workspace?", + "Change &&Workspace", + "Are you sure you want to reload the window?", + "&&Reload Window", + "Are you sure you want to quit?", + "&&Quit", + "'{0}' is in progress.", + "File operation", + "Controls if files that were part of a refactoring are saved automatically" ], "vs/workbench/contrib/files/browser/files.contribution": [ "Text File Editor", @@ -19050,6 +20946,7 @@ "The default end of line character.", "Moves files/folders to the OS trash (recycle bin on Windows) when deleting. Disabling this will delete files/folders permanently.", "When enabled, will trim trailing whitespace when saving a file.", + "When enabled, trailing whitespace will be removed from multiline strings and regexes will be removed on save or when executing 'editor.action.trimTrailingWhitespace'. This can cause whitespace to not be trimmed from lines when there isn't up-to-date token information.", "When enabled, insert a final new line at the end of the file when saving it.", "When enabled, will trim all new lines after the final new line at the end of the file when saving it.", "An editor with changes is never automatically saved.", @@ -19058,6 +20955,8 @@ "An editor with changes is automatically saved when the window loses focus.", "Controls [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors that have unsaved changes.", "Controls the delay in milliseconds after which an editor with unsaved changes is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", + "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that are inside the opened workspace. Only applies when `#files.autoSave#` is enabled.", + "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that have no errors reported in them at the time the auto save is triggered. Only applies when `#files.autoSave#` is enabled.", "Configure paths or [glob patterns](https://aka.ms/vscode-glob-patterns) to exclude from file watching. Paths can either be relative to the watched folder or absolute. Glob patterns are matched relative from the watched folder. When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders).", "Configure extra paths to watch for changes inside the workspace. By default, all workspace folders will be watched recursively, except for folders that are symbolic links. You can explicitly add absolute or relative paths to support watching folders that are symbolic links. Relative paths will be resolved to an absolute path using the currently opened workspace.", "The default language identifier that is assigned to new files. If configured to `${activeEditorLanguage}`, will use the language identifier of the currently active text editor if any.", @@ -19093,6 +20992,7 @@ "Additional check on the siblings of a matching file. Use $(basename) as variable for the matching file name.", "Controls whether the Explorer should allow to move files and folders via drag and drop. This setting only effects drag and drop from inside the Explorer.", "Controls whether the Explorer should ask for confirmation to move files and folders via drag and drop.", + "Controls whether the Explorer should ask for confirmation when pasting native files and folders.", "Controls whether the Explorer should ask for confirmation when deleting a file via the trash.", "Controls whether the Explorer should support undoing file and folder operations.", "Controls whether the Explorer should ask for confirmation when undoing.", @@ -19117,7 +21017,7 @@ "Appends the word \"copy\" at the end of the duplicated name potentially followed by a number.", "Adds a number at the end of the duplicated name. If some number is already part of the name, tries to increase that number.", "Disables incremental naming. If two files with the same name exist you will be prompted to overwrite the existing file.", - "Controls what naming strategy to use when a giving a new name to a duplicated Explorer item on paste.", + "Controls which naming strategy to use when giving a new name to a duplicated Explorer item on paste.", "Controls whether the Explorer should render folders in a compact form. In such a form, single child folders will be compressed in a combined tree element. Useful for Java package structures, for example.", "Use slash as path separation character.", "Use backslash as path separation character.", @@ -19129,30 +21029,11 @@ "Controls nesting of files in the Explorer. {0} must be set for this to take effect. Each __Item__ represents a parent pattern and may contain a single `*` character that matches any string. Each __Value__ represents a comma separated list of the child patterns that should be shown nested under a given parent. Child patterns may contain several special tokens:\n- `${capture}`: Matches the resolved value of the `*` from the parent pattern\n- `${basename}`: Matches the parent file's basename, the `file` in `file.ts`\n- `${extname}`: Matches the parent file's extension, the `ts` in `file.ts`\n- `${dirname}`: Matches the parent file's directory name, the `src` in `src/file.ts`\n- `*`: Matches any string, may only be used once per child pattern", "Each key pattern may contain a single `*` character which will match any string." ], - "vs/workbench/contrib/bulkEdit/browser/bulkEditService": [ - "Made no edits", - "Made {0} text edits in {1} files", - "Made {0} text edits in one file", - "Made {0} text edits in {1} files, also created or deleted {2} files", - "Workspace Edit", - "Workspace Edit", - "Made no edits", - "Are you sure you want to close the window?", - "&&Close Window", - "Are you sure you want to change the workspace?", - "Change &&Workspace", - "Are you sure you want to reload the window?", - "&&Reload Window", - "Are you sure you want to quit?", - "&&Quit", - "'{0}' is in progress.", - "File operation", - "Controls if files that were part of a refactoring are saved automatically" - ], "vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution": [ "Another refactoring is being previewed.", "Press 'Continue' to discard the previous refactoring and continue with the current refactoring.", "&&Continue", + "View icon of the refactor preview view.", "Apply Refactoring", "Refactor Preview", "Discard Refactoring", @@ -19165,44 +21046,19 @@ "Refactor Preview", "Group Changes By Type", "Refactor Preview", - "View icon of the refactor preview view.", "Refactor Preview", "Refactor Preview" ], - "vs/workbench/contrib/searchEditor/browser/searchEditor.contribution": [ - "Search Editor", - "Search Editor", - "Search Editor", - "Delete File Results", - "New Search Editor", - "Open Search Editor", - "Open New Search Editor to the Side", - "Open Results in Editor", - "Search Again", - "Focus Search Editor Input", - "Focus Search Editor Files to Include", - "Focus Search Editor Files to Exclude", - "Toggle Match Case", - "Toggle Match Whole Word", - "Toggle Use Regular Expression", - "Toggle Context Lines", - "Increase Context Lines", - "Decrease Context Lines", - "Select All Matches", - "Open New Search Editor" - ], "vs/workbench/contrib/search/browser/search.contribution": [ - "Search", - "Search", "&&Search", "Search files by name (append {0} to go to line or {1} to go to symbol)", "Go to File", "Type the name of a symbol to open.", "Go to Symbol in Workspace", - "Search for text in your workspace files (experimental).", - "Search for Text (Experimental)", + "Search for text in your workspace files.", + "Search for Text", "Search", - "Configure [glob patterns](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options) for excluding files and folders in fulltext searches and quick open. Inherits all glob patterns from the `#files.exclude#` setting.", + "Configure [glob patterns](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options) for excluding files and folders in fulltext searches and file search in quick open. To exclude files from the recently opened list in quick open, patterns must be absolute (for example `**/node_modules/**`). Inherits all glob patterns from the `#files.exclude#` setting.", "The glob pattern to match file paths against. Set to true or false to enable or disable the pattern.", "Additional check on the siblings of a matching file. Use \\$(basename) as variable for the matching file name.", "Controls where new `Search: Find in Files` and `Find in Folder` operations occur: either in the search view, or in a search editor.", @@ -19214,7 +21070,7 @@ "The search cache is kept in the extension host which never shuts down, so this setting is no longer needed.", "When enabled, the searchService process will be kept alive instead of being shut down after an hour of inactivity. This will keep the file search cache in memory.", "Controls whether to use `.gitignore` and `.ignore` files when searching for files.", - "Controls whether to use your global gitignore file (e.g., from `$HOME/.config/git/ignore`) when searching for files. Requires `#search.useIgnoreFiles#` to be enabled.", + "Controls whether to use your global gitignore file (for example, from `$HOME/.config/git/ignore`) when searching for files. Requires `#search.useIgnoreFiles#` to be enabled.", "Controls whether to use `.gitignore` and `.ignore` files in parent directories when searching for files. Requires `#search.useIgnoreFiles#` to be enabled.", "Whether to include results from a global symbol search in the file results for Quick Open.", "Whether to include results from recently opened files in the file results for Quick Open.", @@ -19244,6 +21100,9 @@ "Double-clicking opens the result in the active editor group.", "Double-clicking opens the result in the editor group to the side, creating one if it does not yet exist.", "Configure effect of double-clicking a result in a search editor.", + "Single-clicking does nothing.", + "Single-clicking opens a Peek Definition window.", + "Configure effect of single-clicking a result in a search editor.", "When enabled, new Search Editors will reuse the includes, excludes, and flags of the previously opened Search Editor.", "The default number of surrounding context lines to use when creating new Search Editors. If using `#search.searchEditor.reusePriorSearchConfiguration#`, this can be set to `null` (empty) to use the prior Search Editor's configuration.", "Results are sorted by folder and file names, in alphabetical order.", @@ -19258,8 +21117,10 @@ "Shows search results as a tree.", "Shows search results as a list.", "Controls the default search result view mode.", + "Controls whether the last typed input to Quick Search should be restored when opening it the next time.", "Show notebook editor rich content results for closed notebooks. Please refresh your search results after changing this setting.", - "Controls whether the last typed input to Quick Search should be restored when opening it the next time." + "Search", + "Search" ], "vs/workbench/contrib/search/browser/searchView": [ "Search was canceled before any results could be found - ", @@ -19287,7 +21148,6 @@ "Replace {0} occurrences across {1} files with '{2}'?", "Replace {0} occurrences across {1} files?", "Empty Search", - "The search results have been cleared", "Search path not found: {0}", "No results found in open editors matching '{0}' excluding '{1}' - ", "No results found in open editors matching '{0}' - ", @@ -19319,29 +21179,125 @@ "You have not opened or specified a folder. Only open files are currently searched - ", "Open Folder" ], + "vs/workbench/contrib/searchEditor/browser/searchEditor.contribution": [ + "Search Editor", + "Search Editor", + "Open New Search Editor", + "Search Editor", + "Delete File Results", + "New Search Editor", + "Open Search Editor", + "Open New Search Editor to the Side", + "Open Results in Editor", + "Search Again", + "Focus Search Editor Input", + "Focus Search Editor Files to Include", + "Focus Search Editor Files to Exclude", + "Toggle Match Case", + "Toggle Match Whole Word", + "Toggle Use Regular Expression", + "Toggle Context Lines", + "Increase Context Lines", + "Decrease Context Lines", + "Select All Matches" + ], "vs/workbench/contrib/sash/browser/sash.contribution": [ "Controls the feedback area size in pixels of the dragging area in between views/editors. Set it to a larger value if you feel it's hard to resize views using the mouse.", "Controls the hover feedback delay in milliseconds of the dragging area in between views/editors." ], - "vs/workbench/contrib/debug/browser/debug.contribution": [ - "Debug", - "Type the name of a launch configuration to run.", - "Start Debugging", - "Type the name of a debug console to open.", - "Show All Debug Consoles", - "Terminate Thread", - "Focus on Debug Console View", - "Jump to Cursor", - "Set Next Statement", - "Inline Breakpoint", + "vs/workbench/contrib/scm/browser/scm.contribution": [ + "View icon of the Source Control view.", + "No source control providers registered.", + "None of the registered source control providers work in Restricted Mode.", + "Manage Workspace Trust", + "Source &&Control", + "Source Control", + "Show the diff decorations in all available locations.", + "Show the diff decorations only in the editor gutter.", + "Show the diff decorations only in the overview ruler.", + "Show the diff decorations only in the minimap.", + "Do not show the diff decorations.", + "Controls diff decorations in the editor.", + "Controls the width(px) of diff decorations in gutter (added & modified).", + "Show the diff decorator in the gutter at all times.", + "Show the diff decorator in the gutter only on hover.", + "Controls the visibility of the Source Control diff decorator in the gutter.", + "Show the inline diff Peek view on click.", + "Do nothing.", + "Controls the behavior of Source Control diff gutter decorations.", + "Controls whether a pattern is used for the diff decorations in gutter.", + "Use pattern for the diff decorations in gutter for added lines.", + "Use pattern for the diff decorations in gutter for modified lines.", + "Ignore leading and trailing whitespace.", + "Do not ignore leading and trailing whitespace.", + "Inherit from `diffEditor.ignoreTrimWhitespace`.", + "Controls whether leading and trailing whitespace is ignored in Source Control diff gutter decorations.", + "Controls whether inline actions are always visible in the Source Control view.", + "Show the sum of all Source Control Provider count badges.", + "Show the count badge of the focused Source Control Provider.", + "Disable the Source Control count badge.", + "Controls the count badge on the Source Control icon on the Activity Bar.", + "Hide Source Control Provider count badges.", + "Only show count badge for Source Control Provider when non-zero.", + "Show Source Control Provider count badges.", + "Controls the count badges on Source Control Provider headers. These headers appear in the Source Control view when there is more than one provider or when the {0} setting is enabled, and in the Source Control Repositories view.", + "Show the repository changes as a tree.", + "Show the repository changes as a list.", + "Controls the default Source Control repository view mode.", + "Sort the repository changes by file name.", + "Sort the repository changes by path.", + "Sort the repository changes by Source Control status.", + "Controls the default Source Control repository changes sort order when viewed as a list.", + "Controls whether the Source Control view should automatically reveal and select files when opening them.", + "Controls the font for the input message. Use `default` for the workbench user interface font family, `editor` for the `#editor.fontFamily#`'s value, or a custom font family.", + "Controls the font size for the input message in pixels.", + "Controls the maximum number of lines that the input will auto-grow to.", + "Controls the minimum number of lines that the input will auto-grow from.", + "Controls whether repositories should always be visible in the Source Control view.", + "Repositories in the Source Control Repositories view are sorted by discovery time. Repositories in the Source Control view are sorted in the order that they were selected.", + "Repositories in the Source Control Repositories and Source Control views are sorted by repository name.", + "Repositories in the Source Control Repositories and Source Control views are sorted by repository path.", + "Controls the sort order of the repositories in the source control repositories view.", + "Controls how many repositories are visible in the Source Control Repositories section. Set to 0, to be able to manually resize the view.", + "Controls whether an action button can be shown in the Source Control view.", + "Controls whether an action button can be shown in the Source Control input.", + "Always show incoming changes in the Source Control view.", + "Never show incoming changes in the Source Control view.", + "Only show incoming changes in the Source Control view when any exist.", + "Controls whether incoming changes are shown in the Source Control view.", + "Always show outgoing changes in the Source Control view.", + "Never show outgoing changes in the Source Control view.", + "Only show outgoing changes in the Source Control view when any exist.", + "Controls whether outgoing changes are shown in the Source Control view.", + "Controls whether the All Changes entry is shown for incoming/outgoing changes in the Source Control view.", + "Controls whether to store editor working sets when switching between source control history item groups.", + "Use an empty working set when switching to a source control history item group that does not have a working set.", + "Use the current working set when switching to a source control history item group that does not have a working set.", + "Controls the default working set to use when switching to a source control history item group that does not have a working set.", + "Source Control: Accept Input", + "Source Control: View Next Commit", + "Source Control: View Previous Commit", + "Open in External Terminal", + "Open in Integrated Terminal", + "Source Control", + "Source Control", + "Source Control Repositories" + ], + "vs/workbench/contrib/debug/browser/debug.contribution": [ + "Debug", + "Type the name of a launch configuration to run.", + "Start Debugging", + "Type the name of a debug console to open.", + "Show All Debug Consoles", "Terminate Thread", "Restart Frame", "Copy Call Stack", "View Binary Data", "Set Value", - "Copy Value", - "Copy as Expression", - "Add to Watch", + "Break on Value Read", + "Break on Value Change", + "Break on Value Access", + "View Binary Data", "Break on Value Read", "Break on Value Change", "Break on Value Access", @@ -19350,7 +21306,6 @@ "Copy Value", "View Binary Data", "Remove Expression", - "Run or Debug...", "&&Run", "&&Start Debugging", "Run &&Without Debugging", @@ -19364,20 +21319,18 @@ "Inline Breakp&&oint", "&&New Breakpoint", "&&Install Additional Debuggers...", - "Debug Console", - "Debug Console", "De&&bug Console", - "Run and Debug", "&&Run", - "Variables", - "Watch", - "Call Stack", - "Breakpoints", - "Loaded Scripts", "Disassembly", "Debug", "Allow setting breakpoints in any file.", + "Controls the action to perform when clicking the editor gutter with the middle mouse button.", + "Add Logpoint.", + "Add Conditional Breakpoint.", + "Add Triggered Breakpoint.", + "Don't perform any action.", "Automatically open the explorer view at the end of a debug session.", + "At the end of a debug session, all the read-only tabs associated with that session will be closed", "Show variable values inline in editor while debugging.", "Always show variable values inline in editor while debugging.", "Never show variable values inline in editor while debugging.", @@ -19421,9 +21374,34 @@ "Always confirm if there are debug sessions.", "Show Source Code in Disassembly View.", "Automatically show values for variables that are lazily resolved by the debugger, such as getters.", - "Color status bar when debugger is active" + "Color of the Status bar when debugger is active.", + "Hide 'Start Debugging' control in title bar of 'Run and Debug' view while debugging is active. Only relevant when `{0}` is not `docked`.", + "Terminate Thread", + "Focus on Debug Console View", + "Jump to Cursor", + "Set Next Statement", + "Inline Breakpoint", + "Run or Debug...", + "Run", + "Debug Console", + "Debug Console", + "Run and Debug", + "Variables", + "Watch", + "Call Stack", + "Breakpoints", + "Loaded Scripts" + ], + "vs/workbench/contrib/debug/browser/callStackEditorContribution": [ + "Background color for the highlight of line at the top stack frame position.", + "Background color for the highlight of line at focused stack frame position." + ], + "vs/workbench/contrib/debug/browser/debugEditorContribution": [ + "Color for the debug inline value text.", + "Color for the debug inline value background." ], "vs/workbench/contrib/debug/browser/breakpointEditorContribution": [ + "Click to add a breakpoint", "Logpoint", "Breakpoint", "This {0} has a {1} that will get lost on remove. Consider enabling the {0} instead.", @@ -19456,6 +21434,7 @@ "Add Breakpoint", "Add Conditional Breakpoint...", "Add Logpoint...", + "Add Triggered Breakpoint...", "Run to Line", "Icon color for breakpoints.", "Icon color for disabled breakpoints.", @@ -19463,153 +21442,54 @@ "Icon color for the current breakpoint stack frame.", "Icon color for all breakpoint stack frames." ], - "vs/workbench/contrib/scm/browser/scm.contribution": [ - "View icon of the Source Control view.", - "Source Control", - "No source control providers registered.", - "None of the registered source control providers work in Restricted Mode.", - "Manage Workspace Trust", - "Source Control", - "Source &&Control", - "Source Control Repositories", - "Source Control Sync", - "Source Control", - "Show the diff decorations in all available locations.", - "Show the diff decorations only in the editor gutter.", - "Show the diff decorations only in the overview ruler.", - "Show the diff decorations only in the minimap.", - "Do not show the diff decorations.", - "Controls diff decorations in the editor.", - "Controls the width(px) of diff decorations in gutter (added & modified).", - "Show the diff decorator in the gutter at all times.", - "Show the diff decorator in the gutter only on hover.", - "Controls the visibility of the Source Control diff decorator in the gutter.", - "Show the inline diff Peek view on click.", - "Do nothing.", - "Controls the behavior of Source Control diff gutter decorations.", - "Controls whether a pattern is used for the diff decorations in gutter.", - "Use pattern for the diff decorations in gutter for added lines.", - "Use pattern for the diff decorations in gutter for modified lines.", - "Ignore leading and trailing whitespace.", - "Do not ignore leading and trailing whitespace.", - "Inherit from `diffEditor.ignoreTrimWhitespace`.", - "Controls whether leading and trailing whitespace is ignored in Source Control diff gutter decorations.", - "Controls whether inline actions are always visible in the Source Control view.", - "Show the sum of all Source Control Provider count badges.", - "Show the count badge of the focused Source Control Provider.", - "Disable the Source Control count badge.", - "Controls the count badge on the Source Control icon on the Activity Bar.", - "Hide Source Control Provider count badges.", - "Only show count badge for Source Control Provider when non-zero.", - "Show Source Control Provider count badges.", - "Controls the count badges on Source Control Provider headers. These headers only appear when there is more than one provider.", - "Show the repository changes as a tree.", - "Show the repository changes as a list.", - "Controls the default Source Control repository view mode.", - "Sort the repository changes by file name.", - "Sort the repository changes by path.", - "Sort the repository changes by Source Control status.", - "Controls the default Source Control repository changes sort order when viewed as a list.", - "Controls whether the Source Control view should automatically reveal and select files when opening them.", - "Controls the font for the input message. Use `default` for the workbench user interface font family, `editor` for the `#editor.fontFamily#`'s value, or a custom font family.", - "Controls the font size for the input message in pixels.", - "Controls whether repositories should always be visible in the Source Control view.", - "Repositories in the Source Control Repositories view are sorted by discovery time. Repositories in the Source Control view are sorted in the order that they were selected.", - "Repositories in the Source Control Repositories and Source Control views are sorted by repository name.", - "Repositories in the Source Control Repositories and Source Control views are sorted by repository path.", - "Controls the sort order of the repositories in the source control repositories view.", - "Controls how many repositories are visible in the Source Control Repositories section. Set to 0, to be able to manually resize the view.", - "Controls whether an action button can be shown in the Source Control view.", - "Controls whether the Source Control Sync view is shown.", - "Source Control: Accept Input", - "Source Control: View Next Commit", - "Source Control: View Previous Commit", - "Open in External Terminal", - "Open in Integrated Terminal" - ], - "vs/workbench/contrib/debug/browser/debugEditorContribution": [ - "Color for the debug inline value text.", - "Color for the debug inline value background." - ], - "vs/workbench/contrib/debug/browser/repl": [ - "Filter (e.g. text, !exclude)", - "Showing {0} of {1}", - "Debug Console", - "Please start a debug session to evaluate expressions", - "REPL Accept Input", - "REPL Focus Content to Filter", - "Debug: Console Copy All", - "Select Debug Console", - "Clear Console", - "Debug console was cleared", - "Collapse All", - "Paste", - "Copy All", - "Copy" - ], - "vs/workbench/contrib/debug/browser/callStackEditorContribution": [ - "Background color for the highlight of line at the top stack frame position.", - "Background color for the highlight of line at focused stack frame position." - ], "vs/workbench/contrib/markers/browser/markers.contribution": [ "View icon of the markers view.", "&&Problems", "View as Tree", "View as Table", - "Toggle Errors", - "Problems", "Show Errors", - "Toggle Warnings", "Problems", "Show Warnings", - "Toggle Infos", "Problems", "Show Infos", - "Toggle Active File", "Problems", "Show Active File Only", - "Toggle Excluded Files", "Problems", - "Hide Excluded Files", - "Copy", - "Copy Message", - "Copy Message", + "Show Excluded Files", + "Problems", "Focus problems view", "Focus problems filter", - "Show message in multiple lines", "Problems", - "Show message in single line", "Problems", "Clear filters text", "Problems", "Collapse All", "Problems", + "Problems are turned off. Click to open settings.", + "Problems Visibility", "Errors: {0}", "Warnings: {0}", "Infos: {0}", "No Problems", "10K+", - "Total {0} Problems" + "Total {0} Problems", + "Copy", + "Copy Message", + "Copy Message", + "Show message in multiple lines", + "Show message in single line" ], "vs/workbench/contrib/debug/browser/debugViewlet": [ "Open &&Configurations", "Select a workspace folder to create a launch.json file in or add it to the workspace config file", "Debug Console", - "Start Additional Session" - ], - "vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution": [ - "Merge Editor", - "Uses the legacy diffing algorithm.", - "Uses the advanced diffing algorithm." - ], - "vs/workbench/contrib/commands/common/commands.contribution": [ - "Run Commands", - "Run several commands", - "Commands to run", - "'runCommands' has received an argument with incorrect type. Please, review the argument passed to the command.", - "'runCommands' has not received commands to run. Did you forget to pass commands in the 'runCommands' argument?" + "Start Additional Session", + "Opens the file used to configure how your program is debugged" ], "vs/workbench/contrib/comments/browser/comments.contribution": [ + "Collapse All", + "Expand All", + "Reply", "Comments", "Controls when the comments panel should open.", "This setting is deprecated in favor of `comments.openView`.", @@ -19622,28 +21502,24 @@ "Controls the visibility of the comments bar and comment threads in editors that have commenting ranges and comments. Comments are still accessible via the Comments view and will cause commenting to be toggled on in the same way running the command \"Comments: Toggle Editor Commenting\" toggles comments.", "Controls whether the comments widget scrolls or expands.", "Controls whether the comment thread should collapse when the thread is resolved.", - "The editor contains commentable range(s). Some useful commands include:", - "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled ({0}).", - "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus, which is currently not triggerable via keybinding.", - "Some useful comment commands include:", - "- Dismiss Comment (Escape)", - "- Go to Next Commenting Range ({0})", - "- Go to Next Commenting Range, which is currently not triggerable via keybinding.", - "- Go to Previous Commenting Range ({0})", - "- Go to Previous Commenting Range, which is currently not triggerable via keybinding.", - "- Go to Next Comment Thread ({0})", - "- Go to Next Comment Thread, which is currently not triggerable via keybinding.", - "- Go to Previous Comment Thread ({0})", - "- Go to Previous Comment Thread, which is currently not triggerable via keybinding.", - "- Add Comment ({0})", - "- Add Comment on Current Selection, which is currently not triggerable via keybinding.", - "- Submit Comment ({0})", - "- Submit Comment, accessible via tabbing, as it's currently not triggerable with a keybinding." + "{0} Unresolved Comments" + ], + "vs/workbench/contrib/commands/common/commands.contribution": [ + "Run several commands", + "Commands to run", + "'runCommands' has received an argument with incorrect type. Please, review the argument passed to the command.", + "'runCommands' has not received commands to run. Did you forget to pass commands in the 'runCommands' argument?", + "Run Commands" + ], + "vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution": [ + "Merge Editor", + "Uses the legacy diffing algorithm.", + "Uses the advanced diffing algorithm." ], "vs/workbench/contrib/url/browser/url.contribution": [ - "Open URL", "URL to open", - "When enabled, trusted domain prompts will appear when opening links in trusted workspaces." + "When enabled, trusted domain prompts will appear when opening links in trusted workspaces.", + "Open URL" ], "vs/workbench/contrib/webview/browser/webview.contribution": [ "Cut", @@ -19653,25 +21529,99 @@ "vs/workbench/contrib/webviewPanel/browser/webviewPanel.contribution": [ "webview editor" ], - "vs/workbench/contrib/extensions/browser/extensions.contribution": [ - "Press Enter to manage extensions.", - "Manage Extensions", - "Extension", - "Extensions", - "E&&xtensions", - "Extensions", - "All Extensions", - "Only Enabled Extensions", - "None", - "Download and install updates automatically for all extensions except for those updates are ignored.", - "Download and install updates automatically only for enabled extensions except for those updates are ignored. Disabled extensions are not updated automatically.", - "Extensions are not automatically updated.", - "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service.", - "When enabled, automatically checks extensions for updates. If an extension has an update, it is marked as outdated in the Extensions view. The updates are fetched from a Microsoft online service.", - "When enabled, the notifications for extension recommendations will not be shown.", - "This setting is deprecated. Use extensions.ignoreRecommendations setting to control recommendation notifications. Use Extensions view's visibility actions to hide Recommended view by default.", - "When enabled, editors with extension details will be automatically closed upon navigating away from the Extensions View.", - "When an extension is listed here, a confirmation prompt will not be shown when that extension handles a URI.", + "vs/workbench/contrib/extensions/browser/extensionsViewlet": [ + "Installed", + "Search Extensions in Marketplace", + "1 extension found in the {0} section.", + "1 extension found.", + "{0} extensions found in the {1} section.", + "{0} extensions found.", + "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting.", + "Open User Settings", + "{0} requires update", + "{0} require update", + "{0} requires restart", + "{0} require restart", + "We have uninstalled '{0}' which was reported to be problematic.", + "Reload Now", + "Remote", + "Install Local Extensions in '{0}'...", + "Install Remote Extensions Locally...", + "Popular", + "Recommended", + "Enabled", + "Disabled", + "Marketplace", + "Installed", + "Recently Updated", + "Enabled", + "Disabled", + "Available Updates", + "Builtin", + "Workspace Unsupported", + "Workspace Recommendations", + "Other Recommendations", + "Features", + "Themes", + "Programming Languages", + "Disabled in Restricted Mode", + "Limited in Restricted Mode", + "Disabled in Virtual Workspaces", + "Limited in Virtual Workspaces", + "Deprecated" + ], + "vs/workbench/contrib/output/browser/outputView": [ + "{0} - Output", + "Output channel for '{0}'", + "Output", + "Output panel" + ], + "vs/workbench/contrib/output/browser/output.contribution": [ + "View icon of the output view.", + "&&Output", + "Switch Output", + "Switch Output", + "Select Output Channel", + "Turn Auto Scrolling Off", + "Turn Auto Scrolling On", + "Set Log Level...", + "Set As Default", + "Extension Logs", + "Select Log", + "The id of the log file to open, for example `\"window\"`. Currently the best way to get this is to get the ID by checking the `workbench.action.output.show.` commands", + "Select Log File", + "Output", + "Enable/disable the ability of smart scrolling in the output view. Smart scrolling allows you to lock scrolling automatically when you click in the output view and unlocks when you click in the last line.", + "Output", + "Output", + "Show Output Channels...", + "Output", + "Clear Output", + "Toggle Auto Scrolling", + "Open Output in Editor", + "Open Output in New Window", + "Show Logs...", + "Open Log File..." + ], + "vs/workbench/contrib/extensions/browser/extensions.contribution": [ + "Press Enter to manage extensions.", + "Manage Extensions", + "Extension", + "E&&xtensions", + "All Extensions", + "Only Enabled Extensions", + "Only Selected Extensions", + "None", + "Download and install updates automatically for all extensions except for those updates are ignored.", + "Download and install updates automatically only for enabled extensions except for those updates are ignored. Disabled extensions are not updated automatically.", + "Download and install updates automatically only for selected extensions.", + "Extensions are not automatically updated.", + "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service.", + "When enabled, automatically checks extensions for updates. If an extension has an update, it is marked as outdated in the Extensions view. The updates are fetched from a Microsoft online service.", + "When enabled, the notifications for extension recommendations will not be shown.", + "This setting is deprecated. Use extensions.ignoreRecommendations setting to control recommendation notifications. Use Extensions view's visibility actions to hide Recommended view by default.", + "When enabled, editors with extension details will be automatically closed upon navigating away from the Extensions View.", + "When an extension is listed here, a confirmation prompt will not be shown when that extension handles a URI.", "The Web Worker Extension Host will always be launched.", "The Web Worker Extension Host will never be launched.", "The Web Worker Extension Host will be launched when a web extension needs it.", @@ -19685,12 +21635,15 @@ "Defines the untrusted workspace support setting for the extension.", "Defines the version of the extension for which the override should be applied. If not specified, the override will be applied independent of the extension version.", "When enabled, extensions which declare the `onStartupFinished` activation event will be activated after a timeout.", + "When enabled, extensions can be searched for via Quick Access and report issues from there.", "Extension '{0}' not found.", "Install the given extension", "Extension id or VSIX resource uri", "When enabled, VS Code installs only newly added extensions from the extension pack VSIX. This option is considered only while installing a VSIX.", "When enabled, VS Code installs the pre-release version of the extension if available.", "When enabled, VS Code do not sync this extension when Settings Sync is on.", + "Justification for installing the extension. This is a string or an object that can be used to pass any information to the installation handlers. i.e. `{reason: 'This extension wants to open a URI', action: 'Open URI'}` will show a message box with the reason and action upon install.", + "When enabled, the extension will be enabled if it is installed but disabled. If the extension is already enabled, this has no effect.", "Context for the installation. This is a JSON object that can be used to pass any information to the installation handlers. i.e. `{skipWalkthrough: true}` will skip opening the walkthrough upon install.", "Extension '{0}' not found.", "Uninstall the given extension", @@ -19704,55 +21657,33 @@ "Install or Search Extensions", "&&Extensions", "Extensions", - "Focus on Extensions View", - "Install Extensions", - "Keymaps", "Migrate Keyboard Shortcuts from...", - "Language Extensions", - "Check for Extension Updates", "All extensions are up to date.", "Auto Update Extensions", "All Extensions", - "Only Enabled Extensions", + "Enabled Extensions", + "Selected Extensions", "None", - "Update All Extensions", - "Disable Auto Update for All Extensions", - "Enable Auto Update for All Extensions", - "Enable All Extensions", - "Enable All Extensions for this Workspace", - "Disable All Installed Extensions", - "Disable All Installed Extensions for this Workspace", - "Install from VSIX...", "Install from VSIX", "&&Install", "Install Extension VSIX", "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", "Completed installing {0} extension from VSIX.", "Reload Now", - "Install Extension from Location...", "Install Extension from Location", "Install", "Location of the web extension", "Install Extension from Location", "Filter Extensions...", - "Show Featured Extensions", "Featured", - "Show Popular Extensions", "Most Popular", - "Show Recommended Extensions", "Recommended", - "Show Recently Published Extensions", "Recently Published", "Category", - "Show Built-in Extensions", "Built-in", - "Show Extension Updates", "Updates", - "Show Extensions Unsupported By Workspace", "Workspace Unsupported", - "Show Enabled Extensions", "Enabled", - "Show Disabled Extensions", "Disabled", "Sort By", "Install Count", @@ -19760,18 +21691,49 @@ "Name", "Published Date", "Updated Date", - "Clear Extensions Search Results", - "Refresh", "Install Workspace Recommended Extensions", - "Show Pre-Release Version", - "Show Release Version", - "Copy", + "Switch to Pre-Release Version", + "Switch to Release Version", "Name: {0}", "Id: {0}", "Description: {0}", "Version: {0}", "Publisher: {0}", "VS Marketplace Link: {0}", + "Extensions", + "Extensions", + "Extensions", + "Extensions", + "Extensions", + "Extensions", + "Focus on Extensions View", + "Install Extensions", + "Keymaps", + "Language Extensions", + "Check for Extension Updates", + "Update All Extensions", + "Disable Auto Update for All Extensions", + "Enable Auto Update for All Extensions", + "Enable All Extensions", + "Enable All Extensions for this Workspace", + "Disable All Installed Extensions", + "Disable All Installed Extensions for this Workspace", + "Install from VSIX...", + "Install Extension from Location...", + "Show Featured Extensions", + "Show Popular Extensions", + "Show Recommended Extensions", + "Show Recently Published Extensions", + "Show Built-in Extensions", + "Show Extension Updates", + "Show Extensions Unsupported By Workspace", + "Show Enabled Extensions", + "Show Disabled Extensions", + "Clear Extensions Search Results", + "Refresh", + "Show Pre-Release Version", + "Show Release Version", + "Copy", "Copy Extension ID", "Extension Settings", "Extension Keyboard Shortcuts", @@ -19782,86 +21744,9 @@ "Add to Workspace Recommendations", "Remove from Workspace Recommendations", "Add Extension to Workspace Recommendations", - "Extensions", "Add Extension to Workspace Folder Recommendations", - "Extensions", "Add Extension to Workspace Ignored Recommendations", - "Extensions", - "Add Extension to Workspace Folder Ignored Recommendations", - "Extensions", - "Extensions" - ], - "vs/workbench/contrib/extensions/browser/extensionsViewlet": [ - "Remote", - "Installed", - "Install Local Extensions in '{0}'...", - "Install Remote Extensions Locally...", - "Popular", - "Recommended", - "Enabled", - "Disabled", - "Marketplace", - "Installed", - "Recently Updated", - "Enabled", - "Disabled", - "Available Updates", - "Builtin", - "Workspace Unsupported", - "Workspace Recommendations", - "Other Recommendations", - "Features", - "Themes", - "Programming Languages", - "Disabled in Restricted Mode", - "Limited in Restricted Mode", - "Disabled in Virtual Workspaces", - "Limited in Virtual Workspaces", - "Deprecated", - "Search Extensions in Marketplace", - "1 extension found in the {0} section.", - "1 extension found.", - "{0} extensions found in the {1} section.", - "{0} extensions found.", - "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting.", - "Open User Settings", - "{0} requires update", - "{0} require update", - "{0} requires reload", - "{0} require reload", - "We have uninstalled '{0}' which was reported to be problematic.", - "Reload Now" - ], - "vs/workbench/contrib/output/browser/output.contribution": [ - "View icon of the output view.", - "Output", - "Output", - "&&Output", - "Switch Output", - "Switch Output", - "Show Output Channels...", - "Output", - "Select Output Channel", - "Clear Output", - "Output was cleared", - "Toggle Auto Scrolling", - "Turn Auto Scrolling Off", - "Turn Auto Scrolling On", - "Open Log Output File", - "Show Logs...", - "Extension Logs", - "Select Log", - "Open Log File...", - "The id of the log file to open, for example `\"window\"`. Currently the best way to get this is to get the ID by checking the `workbench.action.output.show.` commands", - "Select Log File", - "Output", - "Enable/disable the ability of smart scrolling in the output view. Smart scrolling allows you to lock scrolling automatically when you click in the output view and unlocks when you click in the last line." - ], - "vs/workbench/contrib/output/browser/outputView": [ - "{0} - Output", - "Output channel for '{0}'", - "Output", - "Output panel" + "Add Extension to Workspace Folder Ignored Recommendations" ], "vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution": [ "Open in Integrated Terminal", @@ -19889,18 +21774,6 @@ "&&Terminate Task...", "&&Configure Tasks...", "Configure De&&fault Build Task...", - "Open Workspace Tasks", - "Show Task Log", - "Run Task", - "Rerun Last Task", - "Restart Running Task", - "Show Running Tasks", - "Terminate Task", - "Run Build Task", - "Run Test Task", - "Configure Default Build Task", - "Configure Default Test Task", - "Open User Tasks", "User Tasks", "Type the name of a task to run.", "Run Task", @@ -19923,24 +21796,36 @@ "Save all dirty editors before running a task.", "Always saves all editors before running.", "Never saves editors before running.", - "Prompts whether to save editors before running." + "Prompts whether to save editors before running.", + "Enable verbose logging for tasks.", + "Open Workspace Tasks", + "Show Task Log", + "Run Task", + "Rerun Last Task", + "Restart Running Task", + "Show Running Tasks", + "Terminate Task", + "Run Build Task", + "Run Test Task", + "Configure Default Build Task", + "Configure Default Test Task", + "Open User Tasks" ], "vs/workbench/contrib/remote/common/remote.contribution": [ "Workspace does not exist", "Please select another workspace to open.", "&&Open Workspace...", - "Connection: Trigger Reconnect", - "Connection: Pause socket writing", "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine.", "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.", "Remote", "Override the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote. By overriding an extension's default kind using this setting, you specify if that extension should be installed and enabled locally or remotely.", "Restores the ports you forwarded in a workspace.", "When enabled, new running processes are detected and ports that they listen on are automatically forwarded. Disabling this setting will not prevent all ports from being forwarded. Even when disabled, extensions will still be able to cause ports to be forwarded, and opening some URLs will still cause ports to forwarded.", - "Sets the source from which ports are automatically forwarded when {0} is true. On Windows and Mac remotes, the `process` and `hybrid` options have no effect and `output` will be used. Requires a reload to take effect.", + "Sets the source from which ports are automatically forwarded when {0} is true. On Windows and macOS remotes, the `process` and `hybrid` options have no effect and `output` will be used.", "Ports will be automatically forwarded when discovered by watching for processes that are started and include a port.", "Ports will be automatically forwarded when discovered by reading terminal and debug output. Not all processes that use ports will print to the integrated terminal or debug console, so some ports will be missed. Ports forwarded based on output will not be \"un-forwarded\" until reload or until the port is closed by the user in the Ports view.", "Ports will be automatically forwarded when discovered by reading terminal and debug output. Not all processes that use ports will print to the integrated terminal or debug console, so some ports will be missed. Ports will be \"un-forwarded\" by watching for processes that listen on that port to be terminated.", + "The number of auto forwarded ports that will trigger the switch from `process` to `hybrid` when automatically forwarding ports and `remote.autoForwardPortsSource` is set to `process` by default. Set to `0` to disable the fallback. When `remote.autoForwardPortsFallback` hasn't been configured, but `remote.autoForwardPortsSource` has, `remote.autoForwardPortsFallback` will be treated as though it's set to `0`.", "Controls whether local URLs with a port will be forwarded when opened from the terminal and the debug console.", "A port, range of ports (ex. \"40000-55000\"), host and port (ex. \"db:1234\"), or regular expression (ex. \".+\\\\/server.js\"). For a port number or range, the attributes will apply to that port number or range of port numbers. Attributes which use a regular expression will apply to ports whose associated process command line matches the expression.", "Shows a notification when a port is automatically forwarded.", @@ -19970,28 +21855,46 @@ "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "The protocol to use when forwarding this port.", "Set default properties that are applied to all ports that don't get properties from the setting {0}. For example:\n\n```\n{\n \"onAutoForward\": \"ignore\"\n}\n```", - "Specifies the local host name that will be used for port forwarding." - ], - "vs/workbench/contrib/snippets/browser/snippets.contribution": [ - "Controls if surround-with-snippets or file template snippets show as Code Actions.", - "The prefix to use when selecting the snippet in intellisense", - "The snippet is meant to populate or replace a whole file", - "The snippet content. Use `$1`, `${1:defaultText}` to define cursor positions, use `$0` for the final cursor position. Insert variable values with `${varName}` and `${varName:defaultText}`, e.g. `This is file: $TM_FILENAME`.", - "The snippet description.", - "Empty snippet", - "User snippet configuration", - "Empty snippet", - "User snippet configuration", - "A list of language names to which this snippet applies, e.g. 'typescript,javascript'." + "Specifies the local host name that will be used for port forwarding.", + "Connection: Trigger Reconnect", + "Connection: Pause socket writing" ], - "vs/workbench/contrib/folding/browser/folding.contribution": [ - "All", - "All active folding range providers", - "Defines a default folding range provider that takes precedence over all other folding range providers. Must be the identifier of an extension contributing a folding range provider." + "vs/workbench/contrib/debug/browser/repl": [ + "Filter (e.g. text, !exclude, \\escape)", + "Showing {0} of {1}", + "Debug Console", + "Please start a debug session to evaluate expressions", + "REPL Accept Input", + "REPL Focus Content to Filter", + "Debug: Console Copy All", + "Select Debug Console", + "Collapse All", + "Paste", + "Copy All", + "Copy", + "Clear Console", + "Clears all program output from your debug REPL" ], "vs/workbench/contrib/keybindings/browser/keybindings.contribution": [ "Toggle Keyboard Shortcuts Troubleshooting" ], + "vs/workbench/contrib/snippets/browser/snippets.contribution": [ + "Controls if surround-with-snippets or file template snippets show as Code Actions.", + "The prefix to use when selecting the snippet in intellisense", + "The snippet is meant to populate or replace a whole file", + "The snippet content. Use `$1`, `${1:defaultText}` to define cursor positions, use `$0` for the final cursor position. Insert variable values with `${varName}` and `${varName:defaultText}`, e.g. `This is file: $TM_FILENAME`.", + "The snippet description.", + "Empty snippet", + "User snippet configuration", + "Empty snippet", + "User snippet configuration", + "A list of language names to which this snippet applies, e.g. 'typescript,javascript'." + ], + "vs/workbench/contrib/folding/browser/folding.contribution": [ + "All", + "All active folding range providers", + "Defines a default folding range provider that takes precedence over all other folding range providers. Must be the identifier of an extension contributing a folding range provider." + ], "vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution": [ "Configure", "Color Decorator Status", @@ -20008,19 +21911,6 @@ "Read Line With Inline Hints", "Stop Inlay Hints Reading" ], - "vs/workbench/contrib/update/browser/update.contribution": [ - "Show Release Notes", - "Show &&Release Notes", - "This version of {0} does not have release notes online", - "Check for Updates...", - "Download Update", - "Install Update", - "Restart to Update", - "Download {0}", - "Apply Update...", - "Apply Update", - "&&Update" - ], "vs/workbench/contrib/themes/browser/themes.contribution": [ "Icon for the 'Manage' action in the theme selection quick pick.", "Type to Search More. Select to Install. Up/Down Keys to Preview", @@ -20028,29 +21918,31 @@ "This will install extension '{0}' published by '{1}'. Do you want to continue?", "OK", "Installing Extension {0}...", - "Color Theme", + "Select Color Theme for System Dark Mode", + "Select Color Theme for System Light Mode", + "Select Color Theme for High Contrast Dark Mode", + "Select Color Theme for High Contrast Light Mode", + "Select Color Theme (detect system color mode disabled)", + "Detect system color mode enabled. Click to configure.", + "Detect system color mode disabled. Click to configure.", "Install Additional Color Themes...", "Browse Additional Color Themes...", - "Select Color Theme (Up/Down Keys to Preview)", "light themes", "dark themes", "high contrast themes", - "File Icon Theme", "Install Additional File Icon Themes...", "Select File Icon Theme (Up/Down Keys to Preview)", "file icon themes", "None", "Disable File Icons", - "Product Icon Theme", "Install Additional Product Icon Themes...", "Browse Additional Product Icon Themes...", "Select Product Icon Theme (Up/Down Keys to Preview)", "product icon themes", "Default", "Manage Extension", - "Generate Color Theme From Current Settings", - "Toggle between Light/Dark Themes", - "Browse Color Themes in Marketplace", + "Cannot toggle between light and dark themes when `{0}` is enabled in settings.", + "Open Settings", "Themes", "&&Theme", "Color Theme", @@ -20062,7 +21954,30 @@ "Visual Studio Code now ships with a new default theme '{0}'. If you prefer, you can switch back to the old theme or try one of the many other color themes available.", "Try New Theme", "Cancel", - "Visual Studio Code now ships with a new default theme '{0}'. Do you want to give it a try?" + "Visual Studio Code now ships with a new default theme '{0}'. Do you want to give it a try?", + "Color Theme", + "File Icon Theme", + "Product Icon Theme", + "Generate Color Theme From Current Settings", + "Toggle between Light/Dark Themes", + "Browse Color Themes in Marketplace" + ], + "vs/workbench/contrib/update/browser/update.contribution": [ + "Show &&Release Notes", + "This version of {0} does not have release notes online", + "Show &&Release Notes", + "Cannot open the current file as Release Notes", + "Apply Update", + "&&Update", + "Show Release Notes", + "Open Current File as Release Notes", + "Developer", + "Check for Updates...", + "Download Update", + "Install Update", + "Restart to Update", + "Download {0}", + "Apply Update..." ], "vs/workbench/contrib/surveys/browser/nps.contribution": [ "Do you mind taking a quick feedback survey?", @@ -20070,25 +21985,21 @@ "Remind Me Later", "Don't Show Again" ], + "vs/workbench/contrib/surveys/browser/ces.contribution": [ + "Got a moment to help the VS Code team? Please tell us about your experience with VS Code so far.", + "Give Feedback", + "Remind Me Later" + ], "vs/workbench/contrib/surveys/browser/languageSurveys.contribution": [ "Help us improve our support for {0}", "Take Short Survey", "Remind Me Later", "Don't Show Again" ], - "vs/workbench/contrib/surveys/browser/ces.contribution": [ - "Got a moment to help the VS Code team? Please tell us about your experience with VS Code so far.", - "Give Feedback", - "Remind Me Later" - ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution": [ "Welcome", - "Welcome", - "Welcome", - "Go Back", "Mark Step Complete", "Mark Step Incomplete", - "Open Walkthrough...", "Select a walkthrough to open", "The platform of the current workspace, which in remote or serverless contexts may be different from the platform of the UI", "When enabled, an extension's walkthrough will open upon install of the extension.", @@ -20097,9 +22008,15 @@ "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise. Note: This is only observed as a global configuration, it will be ignored if set in a workspace or folder configuration.", "Open a new untitled text file (only applies when opening an empty window).", "Open the Welcome page when opening an empty workbench.", + "Open a new terminal in the editor area.", "Controls which editor is shown at startup, if none are restored from the previous session.", "Deprecated, use the global `workbench.reduceMotion`.", - "When enabled, reduce motion in welcome page." + "When enabled, reduce motion in welcome page.", + "Welcome", + "Opens a Walkthrough to help you get started in VS Code.", + "Welcome", + "Go Back", + "Open Walkthrough..." ], "vs/workbench/contrib/welcomeWalkthrough/browser/walkThrough.contribution": [ "Playground", @@ -20107,18 +22024,29 @@ ], "vs/workbench/contrib/welcomeViews/common/newFile.contribution": [ "Built-In", - "Create", - "New File...", "New File...", "Select File Type or Enter File Name...", "File", "Notebook", "Configure Keybinding", "Create New File ({0})", - "Text File" + "Text File", + "Create", + "New File..." ], - "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline": [ - "Document Symbols" + "vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution": [ + "Whether a call hierarchy provider is available", + "Whether call hierarchy peek is currently showing", + "Whether call hierarchy shows incoming or outgoing calls", + "No results", + "Failed to show call hierarchy", + "Icon for incoming calls in the call hierarchy view.", + "Icon for outgoing calls in the call hierarchy view.", + "Close", + "Peek Call Hierarchy", + "Show Incoming Calls", + "Show Outgoing Calls", + "Refocus Call Hierarchy" ], "vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution": [ "Whether a type hierarchy provider is available", @@ -20126,44 +22054,25 @@ "whether type hierarchy shows super types or subtypes", "No results", "Failed to show type hierarchy", + "Close", "Peek Type Hierarchy", "Show Supertypes", "Show Subtypes", - "Refocus Type Hierarchy", - "Close" - ], - "vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution": [ - "Whether a call hierarchy provider is available", - "Whether call hierarchy peek is currently showing", - "Whether call hierarchy shows incoming or outgoing calls", - "No results", - "Failed to show call hierarchy", - "Peek Call Hierarchy", - "Show Incoming Calls", - "Icon for incoming calls in the call hierarchy view.", - "Show Outgoing Calls", - "Icon for outgoing calls in the call hierarchy view.", - "Refocus Call Hierarchy", - "Close" + "Refocus Type Hierarchy" ], - "vs/workbench/contrib/languageDetection/browser/languageDetection.contribution": [ - "Accept Detected Language: {0}", - "Language Detection", - "Change to Detected Language: {0}", - "Detect Language from Content", - "Unable to detect editor language" + "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline": [ + "Document Symbols" ], "vs/workbench/contrib/outline/browser/outline.contribution": [ "View icon of the outline view.", "Outline", - "Outline", "Render Outline elements with icons.", "Controls whether Outline items are collapsed or expanded.", "Collapse all items.", "Expand all items.", - "Show errors and warnings on Outline elements.", - "Use colors for errors and warnings on Outline elements.", - "Use badges for errors and warnings on Outline elements.", + "Show errors and warnings on Outline elements. Overwritten by `#problems.visibility#` when it is off.", + "Use colors for errors and warnings on Outline elements. Overwritten by `#problems.visibility#` when it is off.", + "Use badges for errors and warnings on Outline elements. Overwritten by `#problems.visibility#` when it is off.", "When enabled, Outline shows `file`-symbols.", "When enabled, Outline shows `module`-symbols.", "When enabled, Outline shows `namespace`-symbols.", @@ -20189,7 +22098,38 @@ "When enabled, Outline shows `struct`-symbols.", "When enabled, Outline shows `event`-symbols.", "When enabled, Outline shows `operator`-symbols.", - "When enabled, Outline shows `typeParameter`-symbols." + "When enabled, Outline shows `typeParameter`-symbols.", + "Outline" + ], + "vs/workbench/contrib/languageDetection/browser/languageDetection.contribution": [ + "Accept Detected Language: {0}", + "Language Detection", + "Change to Detected Language: {0}", + "Unable to detect editor language", + "Detect Language from Content" + ], + "vs/workbench/contrib/languageStatus/browser/languageStatus.contribution": [ + "Editor Language Status", + "Editor Language Status: {0}", + "Add to Status Bar", + "Remove from Status Bar", + "{0}, {1}", + "{0}", + "{0} (Language Status)", + "Reset Language Status Interaction Counter" + ], + "vs/workbench/contrib/authentication/browser/authentication.contribution": [ + "The id of the authentication provider.", + "The human readable name of the authentication provider.", + "Contributes authentication", + "Label", + "ID", + "Authentication", + "No accounts requested yet...", + "An authentication contribution must specify an id.", + "An authentication contribution must specify a label.", + "This authentication id '{0}' has already been registered", + "Loading..." ], "vs/workbench/contrib/userDataSync/browser/userDataSync.contribution": [ "Settings sync is suspended temporarily because the current device is making too many requests. Please reload {0} to resume.", @@ -20202,20 +22142,23 @@ "Settings Sync. Operation Id: {0}", "Show Log" ], + "vs/workbench/contrib/timeline/browser/timeline.contribution": [ + "View icon of the timeline view.", + "Icon for the open timeline action.", + "Timeline", + "The number of items to show in the Timeline view by default and when loading more items. Setting to `null` (the default) will automatically choose a page size based on the visible area of the Timeline view.", + "Experimental. Controls whether the Timeline view will load the next page of items when you scroll to the end of the list.", + "Open Timeline", + "Icon for the filter timeline action.", + "Filter Timeline" + ], "vs/workbench/contrib/editSessions/browser/editSessions.contribution": [ - "Continue Working On...", - "Open In Local Folder", - "Show Log", "Install additional development environment options", "Resuming working changes...", "Storing current working changes...", "Check for pending cloud changes", "Storing working changes...", - "Show Cloud Changes", "Storing your working changes...", - "Resume Latest Changes from Cloud", - "Resume Changes from Serialized Data", - "Store Working Changes in Cloud", "Storing working changes...", "Checking for pending cloud changes...", "There are no changes to resume from the cloud.", @@ -20255,27 +22198,14 @@ "Prompt the user to sign in to store working changes in the cloud with Continue Working On.", "Do not store working changes in the cloud with Continue Working On unless the user has already turned on Cloud Changes.", "Controls whether to prompt the user to store working changes in the cloud when using Continue Working On.", - "Controls whether to surface cloud changes which partially match the current session." - ], - "vs/workbench/contrib/timeline/browser/timeline.contribution": [ - "View icon of the timeline view.", - "Icon for the open timeline action.", - "Timeline", - "The number of items to show in the Timeline view by default and when loading more items. Setting to `null` (the default) will automatically choose a page size based on the visible area of the Timeline view.", - "Experimental. Controls whether the Timeline view will load the next page of items when you scroll to the end of the list.", - "Open Timeline", - "Icon for the filter timeline action.", - "Filter Timeline" - ], - "vs/workbench/contrib/languageStatus/browser/languageStatus.contribution": [ - "Editor Language Status", - "Editor Language Status: {0}", - "Add to Status Bar", - "Remove from Status Bar", - "{0}, {1}", - "{0}", - "{0} (Language Status)", - "Reset Language Status Interaction Counter" + "Controls whether to surface cloud changes which partially match the current session.", + "Continue Working On...", + "Open In Local Folder", + "Show Log", + "Show Cloud Changes", + "Resume Latest Changes from Cloud", + "Resume Changes from Serialized Data", + "Store Working Changes in Cloud" ], "vs/workbench/contrib/workspaces/browser/workspaces.contribution": [ "This folder contains a workspace file '{0}'. Do you want to open it? [Learn more]({1}) about workspace files.", @@ -20283,14 +22213,8 @@ "This folder contains multiple workspace files. Do you want to open one? [Learn more]({0}) about workspace files.", "Select Workspace", "Select a workspace to open", - "Open Workspace", - "This workspace is already open." - ], - "vs/workbench/contrib/deprecatedExtensionMigrator/browser/deprecatedExtensionMigrator.contribution": [ - "The extension 'Bracket pair Colorizer' got disabled because it was deprecated.", - "Uninstall Extension", - "Enable Native Bracket Pair Colorization", - "More Info" + "This workspace is already open.", + "Open Workspace" ], "vs/workbench/contrib/workspace/browser/workspace.contribution": [ "You are trying to open untrusted files in a workspace which is trusted.", @@ -20332,20 +22256,15 @@ "Restricted Mode is intended for safe code browsing. Trust this window to enable all features.", "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features.", "Restricted Mode is intended for safe code browsing. Trust this workspace to enable all features.", - "This window is trusted.", "Restricted Mode: Some features are disabled because this window is not trusted.", "Running in Restricted Mode\n\nSome [features are disabled]({0}) because this [window is not trusted]({1}).", - "This folder is trusted.", "Restricted Mode: Some features are disabled because this folder is not trusted.", "Running in Restricted Mode\n\nSome [features are disabled]({0}) because this [folder is not trusted]({1}).", - "This workspace is trusted.", "Restricted Mode: Some features are disabled because this workspace is not trusted.", "Running in Restricted Mode\n\nSome [features are disabled]({0}) because this [workspace is not trusted]({1}).", "Workspace Trust", + "Restricted Mode", "Workspace Trust Editor", - "Workspaces", - "Configure Workspace Trust Settings", - "Manage Workspace Trust", "Controls whether or not Workspace Trust is enabled within VS Code.", "Controls when the startup prompt to trust a workspace is shown.", "Ask for trust every time an untrusted workspace is opened.", @@ -20359,152 +22278,64 @@ "Ask how to handle untrusted files for each workspace. Once untrusted files are introduced to a trusted workspace, you will not be prompted again.", "Always allow untrusted files to be introduced to a trusted workspace without prompting.", "Always open untrusted files in a separate window in restricted mode without prompting.", - "Controls whether or not the empty window is trusted by default within VS Code. When used with `#{0}#`, you can enable the full functionality of VS Code without prompting in an empty window." + "Controls whether or not the empty window is trusted by default within VS Code. When used with `#{0}#`, you can enable the full functionality of VS Code without prompting in an empty window.", + "Workspaces", + "Configure Workspace Trust Settings", + "Manage Workspace Trust" ], - "vs/workbench/contrib/audioCues/browser/audioCues.contribution": [ - "Enable audio cue when a screen reader is attached.", - "Enable audio cue.", - "Disable audio cue.", - "The volume of the audio cues in percent (0-100).", - "Whether or not position changes should be debounced", - "Plays a sound when the active line has a breakpoint.", - "Plays a sound when the active line has an inline suggestion.", - "Plays a sound when the active line has an error.", - "Plays a sound when the active line has a folded area that can be unfolded.", - "Plays a sound when the active line has a warning.", - "Plays a sound when the debugger stopped on a breakpoint.", - "Plays a sound when trying to read a line with inlay hints that has no inlay hints.", - "Plays a sound when a task is completed.", - "Plays a sound when a task fails (non-zero exit code).", - "Plays a sound when a terminal command fails (non-zero exit code).", - "Plays a sound when terminal Quick Fixes are available.", - "Plays a sound when the focus moves to an inserted line in accessible diff viewer mode or to the next/previous change", - "Plays a sound when the focus moves to a deleted line in accessible diff viewer mode or to the next/previous change", - "Plays a sound when the focus moves to a modified line in accessible diff viewer mode or to the next/previous change", - "Plays a sound when a notebook cell execution is successfully completed.", - "Plays a sound when a notebook cell execution fails.", - "Plays a sound when a chat request is made.", - "Plays a sound on loop while the response is pending.", - "Plays a sound on loop while the response has been received." + "vs/workbench/contrib/deprecatedExtensionMigrator/browser/deprecatedExtensionMigrator.contribution": [ + "The extension 'Bracket pair Colorizer' got disabled because it was deprecated.", + "Uninstall Extension", + "Enable Native Bracket Pair Colorization", + "More Info" ], "vs/workbench/contrib/share/browser/share.contribution": [ - "Share...", "Generating link...", "Copied text to clipboard!", "Copied link to clipboard!", "Close", "Open Link", - "Controls whether to render the Share action next to the command center when {0} is {1}." + "Controls whether to render the Share action next to the command center when {0} is {1}.", + "Share..." ], - "vs/workbench/browser/workbench": [ - "Failed to load a required file. Please restart the application to try again. Details: {0}" + "vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution": [ + "When enabled, available entitlements for the account will be show in the accounts menu.", + "When enabled, the chat panel welcome view will be shown." ], - "vs/workbench/services/configuration/browser/configurationService": [ - "Contribute defaults for configurations", - "Experiments", - "Configure settings to be applied for all profiles." + "vs/workbench/electron-sandbox/actions/developerActions": [ + "Toggle Developer Tools", + "Configure Runtime Arguments", + "Reload With Extensions Disabled", + "Open User Data Folder" ], - "vs/platform/workspace/common/workspace": [ - "Code Workspace" - ], - "vs/workbench/electron-sandbox/window": [ - "Restart", - "Configure", - "Learn More", - "Writing login information to the keychain failed with error '{0}'.", - "Troubleshooting Guide", - "You are running an emulated version of {0}. For better performance download the native arm64 version of {0} build for your machine.", - "Download", - "Proxy Authentication Required", - "&&Log In", - "Username", - "Password", - "The proxy {0} requires a username and password.", - "Remember my credentials", - "Are you sure you want to quit?", - "Are you sure you want to exit?", - "Are you sure you want to close the window?", - "&&Quit", - "&&Exit", - "&&Close Window", - "Do not ask me again", - "Error: {0}", - "The following operations are still running: \n{0}", - "An unexpected error prevented the window to close", - "An unexpected error prevented the application to quit", - "An unexpected error prevented the window to reload", - "An unexpected error prevented to change the workspace", - "Closing the window is taking a bit longer...", - "Quitting the application is taking a bit longer...", - "Reloading the window is taking a bit longer...", - "Changing the workspace is taking a bit longer...", - "Close Anyway", - "Quit Anyway", - "Reload Anyway", - "Change Anyway", - "There is a dependency cycle in the AMD modules that needs to be resolved!", - "It is not recommended to run {0} as root user.", - "Files you store within the installation folder ('{0}') may be OVERWRITTEN or DELETED IRREVERSIBLY without warning at update time.", - "You are running {0} 32-bit, which will soon stop receiving updates on Windows. Consider upgrading to the 64-bit build.", - "Learn More", - "{0}. Use navigation keys to access banner actions.", - "Learn More", - "{0} on {1} will soon stop receiving updates. Consider upgrading your macOS version.", - "Learn More", - "{0}. Use navigation keys to access banner actions.", - "Learn More", - "Resolving shell environment...", - "Learn More" - ], - "vs/workbench/services/remote/electron-sandbox/remoteAgentService": [ - "Open Developer Tools", - "Open in browser", - "Failed to connect to the remote extension host server (Error: {0})" - ], - "vs/platform/workspace/common/workspaceTrust": [ - "Trusted", - "Restricted Mode" - ], - "vs/workbench/services/userDataProfile/common/userDataProfile": [ - "Icon for Default Profile.", - "Profiles", - "Profile" - ], - "vs/workbench/services/log/electron-sandbox/logService": [ - "Window" - ], - "vs/platform/configuration/common/configurationRegistry": [ - "Default Language Configuration Overrides", - "Configure settings to be overridden for the {0} language.", - "Configure editor settings to be overridden for a language.", - "This setting does not support per-language configuration.", - "Configure editor settings to be overridden for a language.", - "This setting does not support per-language configuration.", - "Cannot register an empty property", - "Cannot register '{0}'. This matches property pattern '\\\\[.*\\\\]$' for describing language specific editor settings. Use 'configurationDefaults' contribution.", - "Cannot register '{0}'. This property is already registered.", - "Cannot register '{0}'. The associated policy {1} is already registered with {2}." - ], - "vs/workbench/electron-sandbox/actions/developerActions": [ - "Toggle Developer Tools", - "Configure Runtime Arguments", - "Reload With Extensions Disabled", - "Open User Data Folder" + "vs/platform/configuration/common/configurationRegistry": [ + "Default Language Configuration Overrides", + "Configure settings to be overridden for the {0} language.", + "Configure editor settings to be overridden for a language.", + "This setting does not support per-language configuration.", + "Configure editor settings to be overridden for a language.", + "This setting does not support per-language configuration.", + "Cannot register an empty property", + "Cannot register '{0}'. This matches property pattern '\\\\[.*\\\\]$' for describing language specific editor settings. Use 'configurationDefaults' contribution.", + "Cannot register '{0}'. This property is already registered.", + "Cannot register '{0}'. The associated policy {1} is already registered with {2}." ], "vs/workbench/electron-sandbox/actions/windowActions": [ - "Close Window", "Clos&&e Window", - "Zoom In", "&&Zoom In", - "Zoom Out", "&&Zoom Out", - "Reset Zoom", "&&Reset Zoom", "Close Window", "Close Window", - "Select a window to switch to", + "window group", "{0}, window with unsaved changes", "Current Window", + "Current Window", + "Select a window to switch to", + "Close Window", + "Zoom In", + "Zoom Out", + "Reset Zoom", "Switch Window...", "Quick Switch Window..." ], @@ -20519,13 +22350,12 @@ "Quality type of VS Code", "Whether keyboard focus is inside an input box" ], - "vs/workbench/common/configuration": [ - "Application", - "Workbench", - "Security", - "UNC host names must not contain backslashes.", - "A set of UNC host names (without leading or trailing backslash, for example `192.168.0.1` or `my-server`) to allow without user confirmation. If a UNC host is being accessed that is not allowed via this setting or has not been acknowledged via user confirmation, an error will occur and the operation stopped. A restart is required when changing this setting. Find out more about this setting at https://aka.ms/vscode-windows-unc.", - "If enabled, only allows access to UNC host names that are allowed by the `#security.allowedUNCHosts#` setting or after user confirmation. Find out more about this setting at https://aka.ms/vscode-windows-unc." + "vs/workbench/electron-sandbox/actions/installActions": [ + "Shell command '{0}' successfully installed in PATH.", + "Shell command '{0}' successfully uninstalled from PATH.", + "Shell Command", + "Install '{0}' command in PATH", + "Uninstall '{0}' command from PATH" ], "vs/workbench/common/contextkeys": [ "The kind of workspace opened in the window, either 'empty' (no workspace), 'folder' (single folder) or 'workspace' (multi-root workspace)", @@ -20534,7 +22364,8 @@ "The name of the remote the window is connected to or an empty string if not connected to any remote", "The scheme of the current workspace is from a virtual file system or an empty string.", "The scheme of the current workspace is from a temporary file system.", - "Whether the window is in fullscreen mode", + "Whether the main window is in fullscreen mode", + "Whether an auxiliary window is focused", "The identifier of the embedder according to the product service, if one is defined", "Whether the active editor has unsaved changes", "Whether the active editor is not in preview mode", @@ -20542,6 +22373,7 @@ "Whether the active editor is the last one in its group", "Whether the active editor is pinned", "Whether the active editor is read-only", + "Whether the active compare editor can swap sides", "Whether the active editor can toggle between being read-only or writeable", "Whether the active editor can revert", "The identifier of the active editor", @@ -20555,16 +22387,21 @@ "Whether the active editor group is the last group", "Whether the active editor group is locked", "Whether there are multiple editor groups opened", + "Whether there are multiple editor groups opened in an editor part", + "Editor Part has a maximized group", + "Editor Part is in an auxiliary window", "Whether an editor is open", "Whether Zen mode is enabled", - "Whether centered layout is enabled", + "Whether centered layout is enabled for the main editor", "Whether editors split vertically", - "Whether the editor area is visible", + "Whether the editor area in the main window is visible", "Whether editor tabs are visible", "Whether the sidebar is visible", "Whether the sidebar has keyboard focus", "The identifier of the active viewlet", "Whether the status bar has keyboard focus", + "Style of the window title bar", + "Whether the title bar is visible", "Whether the banner has keyboard focus", "Whether a notification has keyboard focus", "Whether the notifications center is visible", @@ -20589,6 +22426,110 @@ "Whether a resource is present or not", "Whether the resource is backed by a file system provider" ], + "vs/workbench/common/configuration": [ + "Application", + "Workbench", + "Security", + "Problems", + "UNC host names must not contain backslashes.", + "A set of UNC host names (without leading or trailing backslash, for example `192.168.0.1` or `my-server`) to allow without user confirmation. If a UNC host is being accessed that is not allowed via this setting or has not been acknowledged via user confirmation, an error will occur and the operation stopped. A restart is required when changing this setting. Find out more about this setting at https://aka.ms/vscode-windows-unc.", + "If enabled, only allows access to UNC host names that are allowed by the `#security.allowedUNCHosts#` setting or after user confirmation. Find out more about this setting at https://aka.ms/vscode-windows-unc." + ], + "vs/workbench/electron-sandbox/window": [ + "Restart", + "Configure", + "Learn More", + "Writing login information to the keychain failed with error '{0}'.", + "Troubleshooting Guide", + "You are running an emulated version of {0}. For better performance download the native arm64 version of {0} build for your machine.", + "Download", + "Proxy Authentication Required", + "&&Log In", + "Username", + "Password", + "The proxy {0} requires a username and password.", + "Remember my credentials", + "Error: {0}", + "The following operations are still running: \n{0}", + "An unexpected error prevented the window to close", + "An unexpected error prevented the application to quit", + "An unexpected error prevented the window to reload", + "An unexpected error prevented to change the workspace", + "Closing the window is taking a bit longer...", + "Quitting the application is taking a bit longer...", + "Reloading the window is taking a bit longer...", + "Changing the workspace is taking a bit longer...", + "Close Anyway", + "Quit Anyway", + "Reload Anyway", + "Change Anyway", + "There is a dependency cycle in the AMD modules that needs to be resolved!", + "It is not recommended to run {0} as root user.", + "Files you store within the installation folder ('{0}') may be OVERWRITTEN or DELETED IRREVERSIBLY without warning at update time.", + "{0} on {1} will soon stop receiving updates. Consider upgrading your macOS version.", + "Learn More", + "Resolving shell environment...", + "Learn More", + "Zoom Out", + "Zoom In", + "Reset", + "{0} ({1})", + "Settings", + "Window Zoom", + "Zoom Level: {0} ({1}%)" + ], + "vs/workbench/browser/workbench": [ + "Failed to load a required file. Please restart the application to try again. Details: {0}" + ], + "vs/workbench/services/configuration/browser/configurationService": [ + "Contribute defaults for configurations", + "Experiments", + "Configure settings to be applied for all profiles." + ], + "vs/platform/workspace/common/workspace": [ + "Code Workspace" + ], + "vs/workbench/services/remote/electron-sandbox/remoteAgentService": [ + "Open Developer Tools", + "Open in browser", + "Failed to connect to the remote extension host server (Error: {0})" + ], + "vs/workbench/services/log/electron-sandbox/logService": [ + "Window" + ], + "vs/workbench/services/files/electron-sandbox/diskFileSystemProvider": [ + "File Watcher" + ], + "vs/workbench/services/userDataProfile/common/userDataProfile": [ + "Icon for Default Profile.", + "Profile", + "Profiles" + ], + "vs/workbench/browser/parts/dialogs/dialogHandler": [ + "Version: {0}\nCommit: {1}\nDate: {2}\nBrowser: {3}", + "&&Copy", + "OK" + ], + "vs/workbench/electron-sandbox/parts/dialogs/dialogHandler": [ + "Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nElectronBuildId: {4}\nChromium: {5}\nNode.js: {6}\nV8: {7}\nOS: {8}", + "&&Copy", + "OK" + ], + "vs/workbench/services/textfile/browser/textFileService": [ + "File Created", + "File Replaced", + "Text File Model Decorations", + "Deleted, Read-only", + "Read-only", + "Deleted", + "File seems to be binary and cannot be opened as text", + "'{0}' already exists. Do you want to replace it?", + "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", + "&&Replace", + "'{0}' is marked as read-only. Do you want to save anyway?", + "Paths can be configured as read-only via settings.", + "&&Save Anyway" + ], "vs/workbench/services/dialogs/browser/abstractFileDialogService": [ "Your changes will be lost if you don't save them.", "Do you want to save the changes you made to {0}?", @@ -20606,34 +22547,8 @@ "All Files", "No Extension" ], - "vs/workbench/electron-sandbox/actions/installActions": [ - "Shell Command", - "Install '{0}' command in PATH", - "Shell command '{0}' successfully installed in PATH.", - "Uninstall '{0}' command from PATH", - "Shell command '{0}' successfully uninstalled from PATH." - ], - "vs/workbench/services/textfile/browser/textFileService": [ - "File Created", - "File Replaced", - "Text File Model Decorations", - "Deleted, Read-only", - "Read-only", - "Deleted", - "File seems to be binary and cannot be opened as text", - "'{0}' already exists. Do you want to replace it?", - "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", - "&&Replace" - ], - "vs/workbench/browser/parts/dialogs/dialogHandler": [ - "Version: {0}\nCommit: {1}\nDate: {2}\nBrowser: {3}", - "&&Copy", - "OK" - ], - "vs/workbench/electron-sandbox/parts/dialogs/dialogHandler": [ - "Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nElectronBuildId: {4}\nChromium: {5}\nNode.js: {6}\nV8: {7}\nOS: {8}", - "&&Copy", - "OK" + "vs/workbench/services/extensionManagement/common/extensionManagement": [ + "Extensions" ], "vs/workbench/common/theme": [ "Active tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.", @@ -20656,6 +22571,7 @@ "Border to the top of an active tab in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.", "Border to highlight tabs when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.", "Border to highlight tabs in an unfocused group when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.", + "Border between tabs to indicate that a tab can be inserted between two tabs. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.", "Border on the top of modified active tabs in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.", "Border on the top of modified inactive tabs in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.", "Border on the top of modified active tabs in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.", @@ -20665,7 +22581,7 @@ "Border color of an empty editor group that is focused. Editor groups are the containers of editors.", "Background color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.", "Border color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.", - "Background color of the editor group title header when tabs are disabled (`\"workbench.editor.showTabs\": false`). Editor groups are the containers of editors.", + "Background color of the editor group title header when (`\"workbench.editor.showTabs\": \"single\"`). Editor groups are the containers of editors.", "Border color of the editor group title header. Editor groups are the containers of editors.", "Color to separate multiple editor groups from each other. Editor groups are the containers of editors.", "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.", @@ -20686,6 +22602,11 @@ "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.", "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.", "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.", + "Background color of sticky scroll in the panel.", + "Border color of sticky scroll in the panel.", + "Shadow color of sticky scroll in the panel.", + "Output view background color.", + "Output view sticky scroll background color.", "Banner background color. The banner is shown under the title bar of the window.", "Banner foreground color. The banner is shown under the title bar of the window.", "Banner icon color. The banner is shown under the title bar of the window.", @@ -20723,6 +22644,12 @@ "Drag and drop feedback color for the activity bar items. The activity bar is showing on the far left or right and allows to switch between views of the side bar.", "Activity notification badge background color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.", "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.", + "Active foreground color of the item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.", + "Focus border color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.", + "Background color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.", + "Inactive foreground color of the item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.", + "Drag and drop feedback color for the items in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.", + "Background color of the activity bar when set to top / bottom.", "Profile badge background color. The profile badge shows on top of the settings gear icon in the activity bar.", "Profile badge foreground color. The profile badge shows on top of the settings gear icon in the activity bar.", "Background color for the remote indicator on the status bar.", @@ -20738,11 +22665,16 @@ "Side bar background color. The side bar is the container for views like explorer and search.", "Side bar foreground color. The side bar is the container for views like explorer and search.", "Side bar border color on the side separating to the editor. The side bar is the container for views like explorer and search.", + "Side bar title background color. The side bar is the container for views like explorer and search.", "Side bar title foreground color. The side bar is the container for views like explorer and search.", "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.", "Side bar section header background color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.", "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.", "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.", + "Border color between the activity bar at the top/bottom and the views.", + "Background color of sticky scroll in the side bar.", + "Border color of sticky scroll in the side bar.", + "Shadow color of sticky scroll in the side bar.", "Title bar foreground when the window is active.", "Title bar foreground when the window is inactive.", "Title bar background when the window is active.", @@ -20773,227 +22705,6 @@ "The color used for the border of the window when it is active. Only supported in the macOS and Linux desktop client when using the custom title bar.", "The color used for the border of the window when it is inactive. Only supported in the macOS and Linux desktop client when using the custom title bar." ], - "vs/platform/theme/common/colorRegistry": [ - "Overall foreground color. This color is only used if not overridden by a component.", - "Overall foreground for disabled elements. This color is only used if not overridden by a component.", - "Overall foreground color for error messages. This color is only used if not overridden by a component.", - "Foreground color for description text providing additional information, for example for a label.", - "The default color for icons in the workbench.", - "Overall border color for focused elements. This color is only used if not overridden by a component.", - "An extra border around elements to separate them from others for greater contrast.", - "An extra border around active elements to separate them from others for greater contrast.", - "The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor.", - "Color for text separators.", - "Foreground color for links in text.", - "Foreground color for links in text when clicked on and on mouse hover.", - "Foreground color for preformatted text segments.", - "Background color for block quotes in text.", - "Border color for block quotes in text.", - "Background color for code blocks in text.", - "Shadow color of widgets such as find/replace inside the editor.", - "Border color of widgets such as find/replace inside the editor.", - "Input box background.", - "Input box foreground.", - "Input box border.", - "Border color of activated options in input fields.", - "Background color of activated options in input fields.", - "Background hover color of options in input fields.", - "Foreground color of activated options in input fields.", - "Input box foreground color for placeholder text.", - "Input validation background color for information severity.", - "Input validation foreground color for information severity.", - "Input validation border color for information severity.", - "Input validation background color for warning severity.", - "Input validation foreground color for warning severity.", - "Input validation border color for warning severity.", - "Input validation background color for error severity.", - "Input validation foreground color for error severity.", - "Input validation border color for error severity.", - "Dropdown background.", - "Dropdown list background.", - "Dropdown foreground.", - "Dropdown border.", - "Button foreground color.", - "Button separator color.", - "Button background color.", - "Button background color when hovering.", - "Button border color.", - "Secondary button foreground color.", - "Secondary button background color.", - "Secondary button background color when hovering.", - "Badge background color. Badges are small information labels, e.g. for search results count.", - "Badge foreground color. Badges are small information labels, e.g. for search results count.", - "Scrollbar shadow to indicate that the view is scrolled.", - "Scrollbar slider background color.", - "Scrollbar slider background color when hovering.", - "Scrollbar slider background color when clicked on.", - "Background color of the progress bar that can show for long running operations.", - "Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.", - "Foreground color of error squigglies in the editor.", - "If set, color of double underlines for errors in the editor.", - "Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.", - "Foreground color of warning squigglies in the editor.", - "If set, color of double underlines for warnings in the editor.", - "Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.", - "Foreground color of info squigglies in the editor.", - "If set, color of double underlines for infos in the editor.", - "Foreground color of hint squigglies in the editor.", - "If set, color of double underlines for hints in the editor.", - "Border color of active sashes.", - "Editor background color.", - "Editor default foreground color.", - "Sticky scroll background color for the editor", - "Sticky scroll on hover background color for the editor", - "Background color of editor widgets, such as find/replace.", - "Foreground color of editor widgets, such as find/replace.", - "Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.", - "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.", - "Quick picker background color. The quick picker widget is the container for pickers like the command palette.", - "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.", - "Quick picker title background color. The quick picker widget is the container for pickers like the command palette.", - "Quick picker color for grouping labels.", - "Quick picker color for grouping borders.", - "Keybinding label background color. The keybinding label is used to represent a keyboard shortcut.", - "Keybinding label foreground color. The keybinding label is used to represent a keyboard shortcut.", - "Keybinding label border color. The keybinding label is used to represent a keyboard shortcut.", - "Keybinding label border bottom color. The keybinding label is used to represent a keyboard shortcut.", - "Color of the editor selection.", - "Color of the selected text for high contrast.", - "Color of the selection in an inactive editor. The color must not be opaque so as not to hide underlying decorations.", - "Color for regions with the same content as the selection. The color must not be opaque so as not to hide underlying decorations.", - "Border color for regions with the same content as the selection.", - "Color of the current search match.", - "Color of the other search matches. The color must not be opaque so as not to hide underlying decorations.", - "Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.", - "Border color of the current search match.", - "Border color of the other search matches.", - "Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.", - "Color of the Search Editor query matches.", - "Border color of the Search Editor query matches.", - "Color of the text in the search viewlet's completion message.", - "Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.", - "Background color of the editor hover.", - "Foreground color of the editor hover.", - "Border color of the editor hover.", - "Background color of the editor hover status bar.", - "Color of active links.", - "Foreground color of inline hints", - "Background color of inline hints", - "Foreground color of inline hints for types", - "Background color of inline hints for types", - "Foreground color of inline hints for parameters", - "Background color of inline hints for parameters", - "The color used for the lightbulb actions icon.", - "The color used for the lightbulb auto fix actions icon.", - "Background color for text that got inserted. The color must not be opaque so as not to hide underlying decorations.", - "Background color for text that got removed. The color must not be opaque so as not to hide underlying decorations.", - "Background color for lines that got inserted. The color must not be opaque so as not to hide underlying decorations.", - "Background color for lines that got removed. The color must not be opaque so as not to hide underlying decorations.", - "Background color for the margin where lines got inserted.", - "Background color for the margin where lines got removed.", - "Diff overview ruler foreground for inserted content.", - "Diff overview ruler foreground for removed content.", - "Outline color for the text that got inserted.", - "Outline color for text that got removed.", - "Border color between the two text editors.", - "Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views.", - "The background color of unchanged blocks in the diff editor.", - "The foreground color of unchanged blocks in the diff editor.", - "The background color of unchanged code in the diff editor.", - "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree outline color for the focused item when the list/tree is active and selected. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", - "List/Tree background when hovering over items using the mouse.", - "List/Tree foreground when hovering over items using the mouse.", - "List/Tree drag and drop background when moving items around using the mouse.", - "List/Tree foreground color of the match highlights when searching inside the list/tree.", - "List/Tree foreground color of the match highlights on actively focused items when searching inside the list/tree.", - "List/Tree foreground color for invalid items, for example an unresolved root in explorer.", - "Foreground color of list items containing errors.", - "Foreground color of list items containing warnings.", - "Background color of the type filter widget in lists and trees.", - "Outline color of the type filter widget in lists and trees.", - "Outline color of the type filter widget in lists and trees, when there are no matches.", - "Shadow color of the type filter widget in lists and trees.", - "Background color of the filtered match.", - "Border color of the filtered match.", - "Tree stroke color for the indentation guides.", - "Tree stroke color for the indentation guides that are not active.", - "Table border color between columns.", - "Background color for odd table rows.", - "List/Tree foreground color for items that are deemphasized. ", - "Background color of checkbox widget.", - "Background color of checkbox widget when the element it's in is selected.", - "Foreground color of checkbox widget.", - "Border color of checkbox widget.", - "Border color of checkbox widget when the element it's in is selected.", - "Please use quickInputList.focusBackground instead", - "Quick picker foreground color for the focused item.", - "Quick picker icon foreground color for the focused item.", - "Quick picker background color for the focused item.", - "Border color of menus.", - "Foreground color of menu items.", - "Background color of menu items.", - "Foreground color of the selected menu item in menus.", - "Background color of the selected menu item in menus.", - "Border color of the selected menu item in menus.", - "Color of a separator menu item in menus.", - "Toolbar background when hovering over actions using the mouse", - "Toolbar outline when hovering over actions using the mouse", - "Toolbar background when holding the mouse over actions", - "Highlight background color of a snippet tabstop.", - "Highlight border color of a snippet tabstop.", - "Highlight background color of the final tabstop of a snippet.", - "Highlight border color of the final tabstop of a snippet.", - "Color of focused breadcrumb items.", - "Background color of breadcrumb items.", - "Color of focused breadcrumb items.", - "Color of selected breadcrumb items.", - "Background color of breadcrumb item picker.", - "Current header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", - "Current content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", - "Incoming header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", - "Incoming content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", - "Common ancestor header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", - "Common ancestor content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", - "Border color on headers and the splitter in inline merge-conflicts.", - "Current overview ruler foreground for inline merge-conflicts.", - "Incoming overview ruler foreground for inline merge-conflicts.", - "Common ancestor overview ruler foreground for inline merge-conflicts.", - "Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.", - "Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.", - "Minimap marker color for find matches.", - "Minimap marker color for repeating editor selections.", - "Minimap marker color for the editor selection.", - "Minimap marker color for infos.", - "Minimap marker color for warnings.", - "Minimap marker color for errors.", - "Minimap background color.", - "Opacity of foreground elements rendered in the minimap. For example, \"#000000c0\" will render the elements with 75% opacity.", - "Minimap slider background color.", - "Minimap slider background color when hovering.", - "Minimap slider background color when clicked on.", - "The color used for the problems error icon.", - "The color used for the problems warning icon.", - "The color used for the problems info icon.", - "The foreground color used in charts.", - "The color used for horizontal lines in charts.", - "The red color used in chart visualizations.", - "The blue color used in chart visualizations.", - "The yellow color used in chart visualizations.", - "The orange color used in chart visualizations.", - "The green color used in chart visualizations.", - "The purple color used in chart visualizations." - ], "vs/base/common/actions": [ "(empty)" ], @@ -21003,11 +22714,6 @@ "Unable to write into workspace configuration file. Please open the file to correct errors/warnings in it and try again.", "Open Workspace Configuration" ], - "vs/platform/keyboardLayout/common/keyboardConfig": [ - "Keyboard", - "Controls the dispatching logic for key presses to use either `code` (recommended) or `keyCode`.", - "Controls if the AltGraph+ modifier should be treated as Ctrl+Alt+." - ], "vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService": [ "Cannot substitute command variable '{0}' because command did not return a result of type string.", "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", @@ -21017,6 +22723,11 @@ "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue." ], + "vs/platform/keyboardLayout/common/keyboardConfig": [ + "Keyboard", + "Controls the dispatching logic for key presses to use either `code` (recommended) or `keyCode`.", + "Controls if the AltGraph+ modifier should be treated as Ctrl+Alt+." + ], "vs/workbench/services/extensionManagement/common/extensionManagementService": [ "Cannot uninstall extension '{0}'. Extension '{1}' depends on this.", "Cannot uninstall extension '{0}'. Extensions '{1}' and '{2}' depend on this.", @@ -21029,16 +22740,35 @@ "Would you like to install and synchronize extensions across your devices?", "&&Install", "Install (Do &¬ sync)", - "Enabling this extension requires a trusted workspace.", "Trust Workspace & Install", "Install", "Learn More", + "Enabling this extension requires a trusted workspace.", "{0} for the Web", "'{0}' has limited functionality in {1}.", "&&Install Anyway", "&&Show Extensions", "Contains extensions which are not supported.", - "'{0}' contains extensions which are not supported in {1}." + "'{0}' contains extensions which are not supported in {1}.", + "Cannot activate, becase {0} not found" + ], + "vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService": [ + "Can't install release version of '{0}' extension because it has no release version.", + "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2})." + ], + "vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker": [ + "The following editors with unsaved changes could not be saved to the backup location.", + "The following editors with unsaved changes could not be saved or reverted.", + "Try saving or reverting the editors with unsaved changes first and then try again.", + "&&OK", + "Close Anyway", + "Quit Anyway", + "Reload Anyway", + "Backing up editors with unsaved changes is taking a bit longer...", + "Click 'Cancel' to stop waiting and to save or revert editors with unsaved changes.", + "Saving editors with unsaved changes is taking a bit longer...", + "Reverting editors with unsaved changes is taking a bit longer...", + "Discarding backups is taking a bit longer..." ], "vs/workbench/services/workingCopy/common/workingCopyHistoryService": [ "File Saved", @@ -21046,9 +22776,63 @@ "File Renamed", "Saving local history" ], - "vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService": [ - "Can't install release version of '{0}' extension because it has no release version.", - "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2})." + "vs/platform/action/common/actionCommonCategories": [ + "View", + "Help", + "Test", + "File", + "Preferences", + "Developer" + ], + "vs/workbench/services/extensions/common/abstractExtensionService": [ + "The following extensions contain dependency loops and have been disabled: {0}", + "The following extensions contain dependency loops and have been disabled: {0}", + "No extension host found that can launch the test runner at {0}.", + "{0} (Error: {1})", + "The following operation was blocked: {0}", + "The reason for blocking the operation: {0}", + "The reasons for blocking the operation:\n- {0}", + "The remote extension host terminated unexpectedly. Restarting...", + "Remote Extension host terminated unexpectedly 3 times within the last 5 minutes.", + "Restart Remote Extension Host", + "Activation Events" + ], + "vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner": [ + "Extensions have been modified on disk. Please reload the window.", + "Reload Window" + ], + "vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost": [ + "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.", + "Extension host did not start in 10 seconds, that might be a problem.", + "Reload Window", + "Terminating extension debug session" + ], + "vs/workbench/contrib/localization/electron-sandbox/minimalTranslations": [ + "Search language packs in the Marketplace to change the display language to {0}.", + "Search Marketplace", + "Install language pack to change the display language to {0}.", + "Install and Restart" + ], + "vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService": [ + "Changes that you made may not be saved. Please check press 'Cancel' and try again.", + "Unable to open a new window.", + "The browser interrupted the opening of a new window. Press 'Retry' to try again.", + "To avoid this problem in the future, please ensure to allow popups for this website.", + "&&Retry" + ], + "vs/workbench/contrib/localization/common/localization.contribution": [ + "Contributes localizations to the editor", + "Id of the language into which the display strings are translated.", + "Name of the language in English.", + "Name of the language in contributed language.", + "List of translations associated to the language.", + "Id of VS Code or Extension for which this translation is contributed to. Id of VS Code is always `vscode` and of extension should be in format `publisherId.extensionName`.", + "Id should be `vscode` or in format `publisherId.extensionName` for translating VS code or an extension respectively.", + "A relative path to a file containing translations for the language.", + "Language ID", + "Language Name", + "Language Name (Localized)", + "Langauage Packs" ], "vs/editor/common/editorContextKeys": [ "Whether the editor text has focus (cursor is blinking)", @@ -21057,9 +22841,17 @@ "Whether the editor is read-only", "Whether the context is a diff editor", "Whether the context is an embedded diff editor", + "Whether the context is a multi diff editor", + "Whether all files in multi diff editor are collapsed", + "Whether the diff editor has changes", "Whether a moved code block is selected for comparison", "Whether the accessible diff viewer is visible", "Whether the diff editor render side by side inline breakpoint is reached", + "Whether inline mode is active", + "Whether modified is writable in the diff editor", + "Whether modified is writable in the diff editor", + "The uri of the original document", + "The uri of the modified document", "Whether `editor.columnSelection` is enabled", "Whether the editor has text selected", "Whether the editor has multiple selections", @@ -21091,75 +22883,31 @@ "Whether the editor has multiple document formatting providers", "Whether the editor has multiple document selection formatting providers" ], - "vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker": [ - "The following editors with unsaved changes could not be saved to the back up location.", - "The following editors with unsaved changes could not be saved or reverted.", - "Try saving or reverting the editors with unsaved changes first and then try again.", - "Backing up editors with unsaved changes is taking a bit longer...", - "Click 'Cancel' to stop waiting and to save or revert editors with unsaved changes.", - "Saving editors with unsaved changes is taking a bit longer...", - "Reverting editors with unsaved changes is taking a bit longer...", - "Discarding backups is taking a bit longer..." - ], "vs/workbench/common/editor": [ "Text Editor", "Built-in", "Open Anyway", "Configure Limit" ], - "vs/platform/action/common/actionCommonCategories": [ - "View", - "Help", - "Test", - "File", - "Preferences", - "Developer" + "vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard": [ + "Paste Selection Clipboard" ], - "vs/workbench/services/extensions/common/abstractExtensionService": [ - "The following extensions contain dependency loops and have been disabled: {0}", - "The following extensions contain dependency loops and have been disabled: {0}", - "No extension host found that can launch the test runner at {0}.", - "{0} (Error: {1})", - "The following operation was blocked: {0}", - "The reason for blocking the operation: {0}", - "The reasons for blocking the operation:\n- {0}", - "The remote extension host terminated unexpectedly. Restarting...", - "Remote Extension host terminated unexpectedly 3 times within the last 5 minutes.", - "Restart Remote Extension Host" + "vs/workbench/contrib/codeEditor/electron-sandbox/startDebugTextMate": [ + "Start TextMate Syntax Grammar Logging" ], "vs/workbench/contrib/logs/electron-sandbox/logsActions": [ "Open Logs Folder", "Open Extension Logs Folder" ], - "vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner": [ - "Extensions have been modified on disk. Please reload the window.", - "Reload Window" - ], - "vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost": [ - "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.", - "Extension host did not start in 10 seconds, that might be a problem.", - "Reload Window", - "Terminating extension debug session" - ], - "vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard": [ - "Paste Selection Clipboard" - ], "vs/workbench/browser/editor": [ "{0}, preview", "{0}, pinned" ], - "vs/workbench/contrib/codeEditor/electron-sandbox/startDebugTextMate": [ - "Start Text Mate Syntax Grammar Logging" - ], - "vs/workbench/contrib/extensions/common/runtimeExtensionsInput": [ - "Running Extensions" - ], "vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor": [ "Start Extension Host Profile", "Stop Extension Host Profile", "Save Extension Host Profile", - "Save Extension Host Profile", - "Save" + "Save Extension Host Profile" ], "vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction": [ "Start Debugging Extension Host", @@ -21172,6 +22920,10 @@ "Open Extensions Folder", "Cleanup Extensions Folder" ], + "vs/workbench/contrib/extensions/common/runtimeExtensionsInput": [ + "Icon of the runtime extensions editor label.", + "Running Extensions" + ], "vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService": [ "Extension Profiler", "Profiling Extension Host", @@ -21186,21 +22938,23 @@ "The extension '{0}' took a very long time to complete its last operation and it has prevented other extensions from running.", "Show Extensions" ], - "vs/workbench/contrib/localization/electron-sandbox/minimalTranslations": [ - "Search language packs in the Marketplace to change the display language to {0}.", - "Search Marketplace", - "Install language pack to change the display language to {0}.", - "Install and Restart" + "vs/workbench/contrib/issue/common/issue.contribution": [ + "Report &&Issue", + "Report Issue..." ], - "vs/workbench/contrib/localization/common/localization.contribution": [ - "Contributes localizations to the editor", - "Id of the language into which the display strings are translated.", - "Name of the language in English.", - "Name of the language in contributed language.", - "List of translations associated to the language.", - "Id of VS Code or Extension for which this translation is contributed to. Id of VS Code is always `vscode` and of extension should be in format `publisherId.extensionName`.", - "Id should be `vscode` or in format `publisherId.extensionName` for translating VS code or an extension respectively.", - "A relative path to a file containing translations for the language." + "vs/workbench/contrib/terminal/common/terminal": [ + "Contributes terminal functionality.", + "Defines additional terminal profiles that the user can create.", + "The ID of the terminal profile provider.", + "Title for this terminal profile.", + "A codicon, URI, or light and dark URIs to associate with this terminal type.", + "Icon path when a light theme is used", + "Icon path when a dark theme is used" + ], + "vs/workbench/contrib/issue/browser/issueQuickAccess": [ + "Extension Marketplace", + "Extensions", + "Open Extension Page" ], "vs/workbench/services/dialogs/browser/simpleFileDialog": [ "Open Local File...", @@ -21223,19 +22977,6 @@ "Please select a file.", "Please select a folder." ], - "vs/workbench/contrib/issue/common/issue.contribution": [ - "Report Issue...", - "Report &&Issue" - ], - "vs/workbench/contrib/terminal/common/terminal": [ - "Contributes terminal functionality.", - "Defines additional terminal profiles that the user can create.", - "The ID of the terminal profile provider.", - "Title for this terminal profile.", - "A codicon, URI, or light and dark URIs to associate with this terminal type.", - "Icon path when a light theme is used", - "Icon path when a dark theme is used" - ], "vs/editor/common/languages": [ "array", "boolean", @@ -21265,6 +23006,44 @@ "variable", "{0} ({1})" ], + "vs/workbench/services/themes/common/themeConfiguration": [ + "Specifies the color theme used in the workbench when {0} is not enabled.", + "Theme is unknown or not installed.", + "Specifies the color theme when system color mode is dark and {0} is enabled.", + "Theme is unknown or not installed.", + "Specifies the color theme when system color mode is light and {0} is enabled.", + "Theme is unknown or not installed.", + "Specifies the color theme when in high contrast dark mode and {0} is enabled.", + "Theme is unknown or not installed.", + "Specifies the color theme when in high contrast light mode and {0} is enabled.", + "Theme is unknown or not installed.", + "If enabled, will automatically select a color theme based on the system color mode. If the system color mode is dark, {0} is used, else {1}.", + "Overrides colors from the currently selected color theme.", + "Specifies the file icon theme used in the workbench or 'null' to not show any file icons.", + "None", + "No file icons", + "File icon theme is unknown or not installed.", + "Specifies the product icon theme used.", + "Default", + "Default", + "Product icon theme is unknown or not installed.", + "If enabled, will automatically change to high contrast theme if the OS is using a high contrast theme. The high contrast theme to use is specified by {0} and {1}.", + "Sets the colors and styles for comments", + "Sets the colors and styles for strings literals.", + "Sets the colors and styles for keywords.", + "Sets the colors and styles for number literals.", + "Sets the colors and styles for type declarations and references.", + "Sets the colors and styles for functions declarations and references.", + "Sets the colors and styles for variables declarations and references.", + "Sets colors and styles using textmate theming rules (advanced).", + "Whether semantic highlighting should be enabled for this theme.", + "Use `enabled` in `editor.semanticTokenColorCustomizations` setting instead.", + "Use `enabled` in {0} setting instead.", + "Overrides editor syntax colors and font style from the currently selected color theme.", + "Whether semantic highlighting is enabled or disabled for this theme", + "Semantic token styling rules for this theme.", + "Overrides editor semantic token color and styles from the currently selected color theme." + ], "vs/workbench/services/userDataSync/common/userDataSync": [ "Settings", "Keyboard Shortcuts", @@ -21274,15 +23053,10 @@ "UI State", "Profiles", "Workspace State", - "Settings Sync", "View icon of the Settings Sync view.", + "Settings Sync", "Download Settings Sync Activity" ], - "vs/workbench/contrib/tasks/common/tasks": [ - "Whether a task is currently running.", - "Tasks", - "Error: the task identifier '{0}' is missing the required property '{1}'. The task identifier will be ignored." - ], "vs/workbench/contrib/performance/electron-sandbox/startupProfiler": [ "Successfully created profiles.", "Please create an issue and manually attach the following files:\n{0}", @@ -21292,6 +23066,24 @@ "A final restart is required to continue to use '{0}'. Again, thank you for your contribution.", "&&Restart" ], + "vs/workbench/contrib/terminal/common/terminalContextKey": [ + "Whether the terminal is focused.", + "Whether any terminal is focused, including detached terminals used in other UI.", + "Whether a terminal in the editor area is focused.", + "The current number of terminals.", + "Whether the terminal tabs widget is focused.", + "The shell type of the active terminal, this is set to the last known value when no terminals exist.", + "Whether the terminal's alt buffer is active.", + "Whether the terminal's suggest widget is visible.", + "Whether the terminal view is showing", + "Whether text is selected in the active terminal.", + "Whether text is selected in a focused terminal.", + "Whether terminal processes can be launched in the current workspace.", + "Whether one terminal is selected in the terminal tabs list.", + "Whether the focused tab's terminal is a split terminal.", + "Whether the terminal run command picker is currently open.", + "Whether shell integration is enabled in the active terminal" + ], "vs/workbench/contrib/tasks/common/taskService": [ "Whether CustomExecution tasks are supported. Consider using in the when clause of a 'taskDefinition' contribution.", "Whether ShellExecution tasks are supported. Consider using in the when clause of a 'taskDefinition' contribution.", @@ -21300,14 +23092,37 @@ "True when in the web with no remote authority." ], "vs/workbench/common/views": [ + "Views", "Default view icon.", "A view with id '{0}' is already registered", "No tree view with id '{0}' registered." ], + "vs/workbench/contrib/tasks/browser/terminalTaskSystem": [ + "A unknown error has occurred while executing a task. See task output log for details.", + "There are issues with task \"{0}\". See the output for more details.", + "There is a dependency cycle. See task \"{0}\".", + "Couldn't resolve dependent task '{0}' in workspace folder '{1}'", + "Task {0} is a background task but uses a problem matcher without a background pattern", + "Executing task in folder {0}: {1}", + "Executing task: {0}", + "Executing task in folder {0}: {1}", + "Executing task: {0}", + "Executing task: {0}", + "Can't execute a shell command on an UNC drive using cmd.exe.", + "Problem matcher {0} can't be resolved. The matcher will be ignored", + "Press any key to close the terminal.", + "Terminal will be reused by tasks, press any key to close it." + ], "vs/workbench/contrib/tasks/browser/abstractTaskService": [ - "Configure Task", "Tasks", "Select the build task (there is no default build task defined)", + "Task Event kind: {0}", + "Startup kind not window reload, setting connected and removing persistent tasks", + "Setting tasks connected configured value {0}, tasks were already reconnected {1}", + "Reconnecting to running tasks...", + "Reconnected to running tasks.", + "No persistent tasks to reconnect.", + "Reconnecting to {0} tasks...", "Filters the tasks shown in the quickpick", "The task's label or a term to filter by", "The contributed task type", @@ -21315,7 +23130,16 @@ "There are task errors. See the output for details.", "Show output", "The folder {0} is ignored since it uses task version 0.1.0", - "Warning: {0} tasks are unavailable in the current environment.\n", + "Warning: {0} tasks are unavailable in the current environment.", + "Returning cached tasks {0}", + "Fetching tasks from task storage.", + "Reading tasks from task storage, {0}, {1}, {2}", + "Fetching a task from task storage failed: {0}.", + "Resolved task {0}", + "Unable to resolve task {0} ", + "Removing persistent task {0}", + "Setting persistent task {0}", + "Saving persistent tasks: {0}", "No test task defined. Mark a task with 'isTestCommand' in the tasks.json file.", "No test task defined. Mark a task with as a 'test' group in the tasks.json file.", "No build task defined. Mark a task with 'isBuildCommand' in the tasks.json file.", @@ -21329,29 +23153,28 @@ "Select for which kind of errors and warnings to scan the task output", "The current task configuration has errors. Please fix the errors first before customizing a task.", "\t// See https://go.microsoft.com/fwlink/?LinkId=733558 \n\t// for the documentation about the tasks.json format", - "There are many build tasks defined in the tasks.json. Executing the first one.\n", + "There are many build tasks defined in the tasks.json. Executing the first one.", "Save all editors?", "Do you want to save all editors before running the task?", "&&Save", - "Don't save", + "Do&&n't Save", "The task '{0}' is already active.", "Terminate Task", "Restart Task", "There is already a task running. Terminate it first before executing another task.", "Failed to terminate and restart task {0}", "The task provider for \"{0}\" tasks unexpectedly provided a task of type \"{1}\".\n", - "Warning: {0} tasks are unavailable in the current environment.\n", - "Error: The {0} task detection didn't contribute a task for the following configuration:\n{1}\nThe task will be ignored.\n", + "Warning: {0} tasks are unavailable in the current environment.", + "Error: The {0} task detection didn't contribute a task for the following configuration:\n{1}\nThe task will be ignored.", "Error: the provided task configuration has validation errors and can't not be used. Please correct the errors first.", - "Error: The content of the tasks json in {0} has syntax errors. Please correct them before executing a task.\n", + "Error: The content of the tasks json in {0} has syntax errors. Please correct them before executing a task.", "workspace file", "Only tasks version 2.0.0 permitted in workspace configuration files.", "user settings", "Only tasks version 2.0.0 permitted in user settings.", - "Workspace folder was undefined", "Error: the provided task configuration has validation errors and can't not be used. Please correct the errors first.", - "Ignoring task configurations for workspace folder {0}. Multi folder workspace task support requires that all folders use task version 2.0.0\n", - "Error: The content of the tasks.json file has syntax errors. Please correct them before executing a task.\n", + "Ignoring task configurations for workspace folder {0}. Multi folder workspace task support requires that all folders use task version 2.0.0", + "Error: The content of the tasks.json file has syntax errors. Please correct them before executing a task.", "Terminate Task", "An error has occurred while running a task. See task log for details.", "Configure Task", @@ -21392,104 +23215,129 @@ "The deprecated tasks version 0.1.0 has been removed. Your tasks have been upgraded to version 2.0.0. Open the diff to review the upgrade.", "The deprecated tasks version 0.1.0 has been removed. Your tasks have been upgraded to version 2.0.0. Open the diffs to review the upgrade.", "Open diff", - "Open diffs" - ], - "vs/workbench/contrib/tasks/browser/terminalTaskSystem": [ - "A unknown error has occurred while executing a task. See task output log for details.", - "There are issues with task \"{0}\". See the output for more details.", - "There is a dependency cycle. See task \"{0}\".", - "Couldn't resolve dependent task '{0}' in workspace folder '{1}'", - "Task {0} is a background task but uses a problem matcher without a background pattern", - "Executing task in folder {0}: {1}", - "Executing task: {0}", - "Executing task in folder {0}: {1}", - "Executing task: {0}", - "Executing task: {0}", - "Can't execute a shell command on an UNC drive using cmd.exe.", - "Problem matcher {0} can't be resolved. The matcher will be ignored", - "Press any key to close the terminal.", - "Terminal will be reused by tasks, press any key to close it." + "Open diffs", + "Configure Task" ], - "vs/platform/audioCues/browser/audioCueService": [ + "vs/platform/accessibilitySignal/browser/accessibilitySignalService": [ + "Error at Position", + "Error", + "Warning at Position", + "Warning", "Error on Line", + "Error on Line", + "Warning on Line", "Warning on Line", "Folded Area on Line", + "Folded", "Breakpoint on Line", + "Breakpoint", "Inline Suggestion on Line", "Terminal Quick Fix", + "Quick Fix", "Debugger Stopped on Breakpoint", + "Breakpoint", "No Inlay Hints on Line", + "No Inlay Hints", + "Task Completed", "Task Completed", "Task Failed", + "Task Failed", "Terminal Command Failed", + "Command Failed", + "Terminal Bell", "Terminal Bell", "Notebook Cell Completed", + "Notebook Cell Completed", + "Notebook Cell Failed", "Notebook Cell Failed", "Diff Line Inserted", "Diff Line Deleted", "Diff Line Modified", "Chat Request Sent", + "Chat Request Sent", "Chat Response Received", - "Chat Response Pending" + "Progress", + "Progress", + "Clear", + "Clear", + "Save", + "Save", + "Format", + "Format", + "Voice Recording Started", + "Voice Recording Stopped" + ], + "vs/workbench/contrib/tasks/common/tasks": [ + "Whether a task is currently running.", + "Error: the task identifier '{0}' is missing the required property '{1}'. The task identifier will be ignored.", + "Tasks" ], "vs/workbench/contrib/webview/electron-sandbox/webviewCommands": [ - "Open Webview Developer Tools", - "Using standard dev tools to debug iframe based webview" + "Opens Developer Tools for active webviews", + "Using standard dev tools to debug iframe based webview", + "Open Webview Developer Tools" ], - "vs/workbench/contrib/terminal/common/terminalContextKey": [ - "Whether the terminal is focused.", - "Whether any terminal is focused, including detached terminals used in other UI.", - "Whether a terminal in the editor area is focused.", - "The current number of terminals.", - "Whether the terminal tabs widget is focused.", - "The shell type of the active terminal, this is set to the last known value when no terminals exist.", - "Whether the terminal's alt buffer is active.", - "Whether the terminal's suggest widget is visible.", - "Whether the terminal view is showing", - "Whether text is selected in the active terminal.", - "Whether text is selected in a focused terminal.", - "Whether terminal processes can be launched in the current workspace.", - "Whether one terminal is selected in the terminal tabs list.", - "Whether the focused tab's terminal is a split terminal.", - "Whether the terminal run command picker is currently open.", - "Whether shell integration is enabled in the active terminal" + "vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands": [ + "Enter JSON", + "Merge Editor (Dev)", + "Open Merge Editor State from JSON", + "Open Selection In Temporary Merge Editor" ], "vs/workbench/contrib/localHistory/electron-sandbox/localHistoryCommands": [ "Reveal in File Explorer", "Reveal in Finder", "Open Containing Folder" ], - "vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands": [ - "Merge Editor (Dev)", - "Open Merge Editor State from JSON", - "Enter JSON", - "Open Selection In Temporary Merge Editor" + "vs/workbench/contrib/multiDiffEditor/browser/actions": [ + "Open File", + "Collapse All Diffs", + "Expand All Diffs" + ], + "vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver": [ + "View Changes" + ], + "vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions": [ + "Hold for Speech" + ], + "vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput": [ + "Multi Diff Editor", + " ({0} files)" ], "vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions": [ "True when getting ready for receiving voice input from the microphone for voice chat.", "True when voice recording from microphone is in progress for voice chat.", "True when voice recording from microphone is in progress for quick chat.", "True when voice recording from microphone is in progress for inline chat.", + "True when voice recording from microphone is in progress for terminal chat.", "True when voice recording from microphone is in progress in the chat view.", "True when voice recording from microphone is in progress in the chat editor.", - "Voice Chat in Chat View", + "I'm listening", + "Microphone support requires this extension.", + "Keyword activation is disabled.", + "Keyword activation is enabled and listening for 'Hey Code' to start a voice chat session in the chat view.", + "Keyword activation is enabled and listening for 'Hey Code' to start a voice chat session in the quick chat.", + "Keyword activation is enabled and listening for 'Hey Code' to start a voice chat session in the active editor if possible.", + "Keyword activation is enabled and listening for 'Hey Code' to start a voice chat session in the active editor or view depending on keyboard focus.", + "Controls whether the keyword phrase 'Hey Code' is recognized to start a voice chat session. Enabling this will start recording from the microphone but the audio is processed locally and never sent to a server.", + "Voice Keyword Activation", + "Listening to 'Hey Code'...", + "Waiting for voice chat to end...", + "Voice Chat in View", + "Hold to Voice Chat in View", "Inline Voice Chat", "Quick Voice Chat", "Start Voice Chat", - "Stop Voice Chat", - "Stop Voice Chat (Chat View)", - "Stop Voice Chat (Chat Editor)", - "Stop Voice Chat (Quick Chat)", - "Stop Voice Chat (Inline Editor)", - "Stop Voice Chat and Submit" + "Start Voice Chat", + "Stop Listening", + "Stop Listening and Submit" + ], + "vs/workbench/api/common/extHostTelemetry": [ + "Extension Telemetry{0}" ], "vs/workbench/api/common/extHostExtensionService": [ "Cannot load test runner.", "Path {0} does not point to a valid extension test runner." ], - "vs/workbench/api/common/extHostTelemetry": [ - "Extension Telemetry{0}" - ], "vs/workbench/api/common/extHostWorkspace": [ "Extension '{0}' failed to update workspace folders: {1}" ], @@ -21505,8 +23353,9 @@ "Extension Host (Worker)", "Extension Host" ], - "vs/workbench/api/node/extHostDebugService": [ - "Debug Process" + "vs/workbench/api/common/extHostLanguageModels": [ + "To allow access to the language models provided by {0}. Justification:\n\n{1}", + "To allow access to the language models provided by {0}" ], "vs/platform/terminal/node/terminalProcess": [ "Starting directory (cwd) \"{0}\" is not a directory", @@ -21514,10 +23363,8 @@ "Path to shell executable \"{0}\" does not exist", "Path to shell executable \"{0}\" is not a file or a symlink" ], - "vs/platform/shell/node/shellEnv": [ - "Unable to resolve your shell environment in a reasonable time. Please review your shell configuration and restart.", - "Unable to resolve your shell environment: {0}", - "Unexpected exit code from spawned shell (code {0}, signal {1})" + "vs/workbench/api/node/extHostDebugService": [ + "Debug Process" ], "vs/platform/dialogs/electron-main/dialogMainService": [ "Open", @@ -21526,6 +23373,11 @@ "Open Workspace from File", "&&Open" ], + "vs/platform/shell/node/shellEnv": [ + "Unable to resolve your shell environment in a reasonable time. Please review your shell configuration and restart.", + "Unable to resolve your shell environment: {0}", + "Unexpected exit code from spawned shell (code {0}, signal {1})" + ], "vs/platform/externalTerminal/node/externalTerminalService": [ "VS Code Console", "Script '{0}' failed with exit code {1}", @@ -21535,8 +23387,8 @@ "can't find terminal application '{0}'" ], "vs/platform/files/electron-main/diskFileSystemProviderServer": [ - "Failed to move '{0}' to the recycle bin", - "Failed to move '{0}' to the trash" + "Failed to move '{0}' to the recycle bin ({1})", + "Failed to move '{0}' to the trash ({1})" ], "vs/platform/issue/electron-main/issueMainService": [ "Local", @@ -21563,6 +23415,23 @@ "Unable to uninstall the shell command '{0}'.", "Unable to find shell script in '{0}'" ], + "vs/platform/workspaces/electron-main/workspacesHistoryMainService": [ + "&&Clear", + "&&Cancel", + "Do you want to clear all recently opened files and workspaces?", + "This action is irreversible!", + "New Window", + "Opens a new window", + "Recent Folders & Workspaces", + "Recent Folders", + "Untitled (Workspace)", + "{0} (Workspace)" + ], + "vs/platform/workspaces/electron-main/workspacesManagementMainService": [ + "&&OK", + "Unable to save workspace '{0}'", + "The workspace is already opened in another window. Please close that window first and then try again." + ], "vs/platform/windows/electron-main/windowsMainService": [ "&&OK", "Path does not exist", @@ -21576,25 +23445,9 @@ "The path '{0}' uses a host that is not allowed. Unless you trust the host, you should press 'Cancel'", "Permanently allow host '{0}'" ], - "vs/platform/workspaces/electron-main/workspacesManagementMainService": [ - "&&OK", - "Unable to save workspace '{0}'", - "The workspace is already opened in another window. Please close that window first and then try again." - ], - "vs/platform/workspaces/electron-main/workspacesHistoryMainService": [ - "New Window", - "Opens a new window", - "Recent Folders & Workspaces", - "Recent Folders", - "Untitled (Workspace)", - "{0} (Workspace)" - ], "vs/platform/files/common/io": [ "File is too large to open" ], - "vs/base/browser/ui/button/button": [ - "More Actions..." - ], "vs/platform/extensions/common/extensionValidator": [ "property publisher must be of type `string`.", "property `{0}` is mandatory and must be of type `string`", @@ -21648,6 +23501,28 @@ "Cannot uninstall '{0}' extension. It includes uninstalling '{1}' extension and '{2}' and '{3}' extensions depend on this.", "Cannot uninstall '{0}' extension. It includes uninstalling '{1}' extension and '{2}', '{3}' and other extensions depend on this." ], + "vs/platform/extensionManagement/node/extensionManagementUtil": [ + "VSIX invalid: package.json is not a JSON file." + ], + "vs/base/browser/ui/button/button": [ + "More Actions..." + ], + "vs/platform/theme/common/iconRegistry": [ + "The id of the font to use. If not set, the font that is defined first is used.", + "The font character associated with the icon definition.", + "Icon for the close action in widgets.", + "Icon for goto previous editor location.", + "Icon for goto next editor location." + ], + "vs/base/browser/ui/tree/abstractTree": [ + "Filter", + "Fuzzy Match", + "Type to filter", + "Type to search", + "Type to search", + "Close", + "No elements found." + ], "vs/base/common/date": [ "in {0}", "now", @@ -21702,10 +23577,16 @@ "{0} year", "{0} yr", "{0} years", - "{0} yrs" - ], - "vs/platform/extensionManagement/node/extensionManagementUtil": [ - "VSIX invalid: package.json is not a JSON file." + "{0} yrs", + "{0} milliseconds", + "{0}ms", + "{0} seconds", + "{0}s", + "{0} minutes", + "{0} mins", + "{0} hours", + "{0} hrs", + "{0} days" ], "vs/platform/userDataSync/common/keybindingsSync": [ "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it.", @@ -21727,22 +23608,6 @@ "Cannot sync because current session is expired", "Cannot sync because syncing is turned off on this machine from another machine." ], - "vs/base/browser/ui/tree/abstractTree": [ - "Filter", - "Fuzzy Match", - "Type to filter", - "Type to search", - "Type to search", - "Close", - "No elements found." - ], - "vs/platform/theme/common/iconRegistry": [ - "The id of the font to use. If not set, the font that is defined first is used.", - "The font character associated with the icon definition.", - "Icon for the close action in widgets.", - "Icon for goto previous editor location.", - "Icon for goto next editor location." - ], "vs/editor/common/core/editorColorRegistry": [ "Background color for the highlight of line at the cursor position.", "Background color for the border around the line at the cursor position.", @@ -21752,6 +23617,10 @@ "Background color of the border around highlighted symbols.", "Color of the editor cursor.", "The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.", + "Color of the primary editor cursor when multiple cursors are present.", + "The background color of the primary editor cursor when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.", + "Color of secondary editor cursors when multiple cursors are present.", + "The background color of secondary editor cursors when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.", "Color of whitespace characters in the editor.", "Color of editor line numbers.", "Color of the editor indentation guides.", @@ -21812,22 +23681,6 @@ "Border color used to highlight unicode characters.", "Background color used to highlight unicode characters." ], - "vs/editor/browser/widget/diffEditor/diffEditor.contribution": [ - "Toggle Collapse Unchanged Regions", - "Toggle Show Moved Code Blocks", - "Toggle Use Inline View When Space Is Limited", - "Use Inline View When Space Is Limited", - "Show Moved Code Blocks", - "Diff Editor", - "Switch Side", - "Exit Compare Move", - "Collapse All Unchanged Regions", - "Show All Unchanged Regions", - "Accessible Diff Viewer", - "Go to Next Difference", - "Open Accessible Diff Viewer", - "Go to Previous Difference" - ], "vs/platform/contextkey/common/scanner": [ "Did you mean {0}?", "Did you mean {0} or {1}?", @@ -21835,6 +23688,13 @@ "Did you forget to open or close the quote?", "Did you forget to escape the '/' (slash) character? Put two backslashes before it to escape, e.g., '\\\\/'." ], + "vs/editor/browser/widget/diffEditor/diffEditor.contribution": [ + "Use Inline View When Space Is Limited", + "Show Moved Code Blocks", + "Revert Block", + "Revert Selection", + "Open Accessible Diff Viewer" + ], "vs/editor/browser/coreCommands": [ "Stick to the end even when going to longer lines", "Stick to the end even when going to longer lines", @@ -21848,20 +23708,17 @@ "Select from Anchor to Cursor", "Cancel Selection Anchor" ], - "vs/editor/contrib/caretOperations/browser/caretOperations": [ - "Move Selected Text Left", - "Move Selected Text Right" - ], "vs/editor/contrib/bracketMatching/browser/bracketMatching": [ "Overview ruler marker color for matching brackets.", "Go to Bracket", "Select to Bracket", "Remove Brackets", - "Go to &&Bracket" + "Go to &&Bracket", + "Select the text inside and including the brackets or curly braces" ], - "vs/editor/browser/widget/codeEditorWidget": [ - "The number of cursors has been limited to {0}. Consider using [find and replace](https://code.visualstudio.com/docs/editor/codebasics#_find-and-replace) for larger changes or increase the editor multi cursor limit setting.", - "Increase Multi Cursor Limit" + "vs/editor/contrib/caretOperations/browser/caretOperations": [ + "Move Selected Text Left", + "Move Selected Text Right" ], "vs/editor/contrib/caretOperations/browser/transpose": [ "Transpose Letters" @@ -21875,31 +23732,29 @@ "Copy", "Copy", "Copy", - "Copy As", - "Copy As", - "Share", - "Share", - "Share", "&&Paste", "Paste", "Paste", "Paste", - "Copy With Syntax Highlighting" + "Copy With Syntax Highlighting", + "Copy As", + "Copy As", + "Share", + "Share", + "Share" + ], + "vs/editor/browser/widget/codeEditor/codeEditorWidget": [ + "The number of cursors has been limited to {0}. Consider using [find and replace](https://code.visualstudio.com/docs/editor/codebasics#_find-and-replace) for larger changes or increase the editor multi cursor limit setting.", + "Increase Multi Cursor Limit" ], "vs/editor/contrib/codeAction/browser/codeActionContributions": [ "Enable/disable showing group headers in the Code Action menu.", - "Enable/disable showing nearest quickfix within a line when not currently on a diagnostic." + "Enable/disable showing nearest Quick Fix within a line when not currently on a diagnostic." ], "vs/editor/contrib/codelens/browser/codelensController": [ "Show CodeLens Commands For Current Line", "Select a command" ], - "vs/editor/contrib/colorPicker/browser/standaloneColorPickerActions": [ - "Show or Focus Standalone Color Picker", - "&&Show or Focus Standalone Color Picker", - "Hide the Color Picker", - "Insert Color with Standalone Color Picker" - ], "vs/editor/contrib/comment/browser/comment": [ "Toggle Line Comment", "&&Toggle Line Comment", @@ -21908,6 +23763,19 @@ "Toggle Block Comment", "Toggle &&Block Comment" ], + "vs/editor/contrib/colorPicker/browser/standaloneColorPickerActions": [ + "&&Show or Focus Standalone Color Picker", + "Hide the Color Picker", + "Insert Color with Standalone Color Picker", + "Show or Focus Standalone Color Picker", + "Show or focus a standalone color picker which uses the default color provider. It displays hex/rgb/hsl colors.", + "Hide the standalone color picker.", + "Insert hex/rgb/hsl colors with the focused standalone color picker." + ], + "vs/editor/contrib/cursorUndo/browser/cursorUndo": [ + "Cursor Undo", + "Cursor Redo" + ], "vs/editor/contrib/contextmenu/browser/contextmenu": [ "Minimap", "Render Characters", @@ -21920,25 +23788,15 @@ "Always", "Show Editor Context Menu" ], - "vs/editor/contrib/cursorUndo/browser/cursorUndo": [ - "Cursor Undo", - "Cursor Redo" - ], "vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution": [ + "The kind of the paste edit to try applying. If not provided or there are multiple edits for this kind, the editor will show a picker.", "Paste As...", - "The id of the paste edit to try applying. If not provided, the editor will show a picker." - ], - "vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution": [ - "Configures the default drop provider to use for content of a given mime type." + "Paste as Text" ], "vs/editor/contrib/find/browser/findController": [ "The file is too large to perform a replace all operation.", "Find", "&&Find", - "Overrides \"Use Regular Expression\" flag.\nThe flag will not be saved for the future.\n0: Do Nothing\n1: True\n2: False", - "Overrides \"Match Whole Word\" flag.\nThe flag will not be saved for the future.\n0: Do Nothing\n1: True\n2: False", - "Overrides \"Math Case\" flag.\nThe flag will not be saved for the future.\n0: Do Nothing\n1: True\n2: False", - "Overrides \"Preserve Case\" flag.\nThe flag will not be saved for the future.\n0: Do Nothing\n1: True\n2: False", "Find With Arguments", "Find With Selection", "Find Next", @@ -21953,10 +23811,13 @@ "Replace", "&&Replace" ], + "vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution": [ + "Configures the default drop provider to use for content of a given mime type." + ], "vs/editor/contrib/fontZoom/browser/fontZoom": [ - "Editor Font Zoom In", - "Editor Font Zoom Out", - "Editor Font Zoom Reset" + "Increase Editor Font Size", + "Decrease Editor Font Size", + "Reset Editor Font Size" ], "vs/editor/contrib/folding/browser/folding": [ "Unfold", @@ -21982,49 +23843,49 @@ "Format Document", "Format Selection" ], + "vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition": [ + "Click to show {0} definitions." + ], "vs/editor/contrib/gotoSymbol/browser/goToCommands": [ "Peek", "Definitions", "No definition found for '{0}'", "No definition found", - "Go to Definition", "Go to &&Definition", - "Open Definition to the Side", - "Peek Definition", "Declarations", "No declaration found for '{0}'", "No declaration found", - "Go to Declaration", "Go to &&Declaration", "No declaration found for '{0}'", "No declaration found", - "Peek Declaration", "Type Definitions", "No type definition found for '{0}'", "No type definition found", - "Go to Type Definition", "Go to &&Type Definition", - "Peek Type Definition", "Implementations", "No implementation found for '{0}'", "No implementation found", - "Go to Implementations", "Go to &&Implementations", - "Peek Implementations", "No references found for '{0}'", "No references found", - "Go to References", "Go to &&References", "References", - "Peek References", "References", - "Go to Any Symbol", "Locations", "No results for '{0}'", - "References" - ], - "vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition": [ - "Click to show {0} definitions." + "References", + "Go to Definition", + "Open Definition to the Side", + "Peek Definition", + "Go to Declaration", + "Peek Declaration", + "Go to Type Definition", + "Peek Type Definition", + "Go to Implementations", + "Peek Implementations", + "Go to References", + "Peek References", + "Go to Any Symbol" ], "vs/editor/contrib/gotoError/browser/gotoError": [ "Go to Next Problem (Error, Warning, Info)", @@ -22048,42 +23909,22 @@ "Change Tab Display Size", "Detect Indentation from Content", "Reindent Lines", - "Reindent Selected Lines" - ], - "vs/editor/contrib/hover/browser/hover": [ - "Show or Focus Hover", - "Show Definition Preview Hover", - "Scroll Up Hover", - "Scroll Down Hover", - "Scroll Left Hover", - "Scroll Right Hover", - "Page Up Hover", - "Page Down Hover", - "Go To Top Hover", - "Go To Bottom Hover" - ], - "vs/editor/contrib/lineSelection/browser/lineSelection": [ - "Expand Line Selection" - ], - "vs/editor/contrib/linkedEditing/browser/linkedEditing": [ - "Start Linked Editing", - "Background color when the editor auto renames on type." + "Reindent Selected Lines", + "Convert the tab indentation to spaces.", + "Convert the spaces indentation to tabs.", + "Use indentation with tabs.", + "Use indentation with spaces.", + "Change the space size equivalent of the tab.", + "Detect the indentation from content.", + "Reindent the lines of the editor.", + "Reindent the selected lines of the editor." ], "vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace": [ "Replace with Previous Value", "Replace with Next Value" ], - "vs/editor/contrib/links/browser/links": [ - "Failed to open this link because it is not well-formed: {0}", - "Failed to open this link because its target is missing.", - "Execute command", - "Follow link", - "cmd + click", - "ctrl + click", - "option + click", - "alt + click", - "Execute command {0}", - "Open Link" + "vs/editor/contrib/lineSelection/browser/lineSelection": [ + "Expand Line Selection" ], "vs/editor/contrib/linesOperations/browser/linesOperations": [ "Copy Line Up", @@ -22114,8 +23955,25 @@ "Transform to Title Case", "Transform to Snake Case", "Transform to Camel Case", + "Transform to Pascal Case", "Transform to Kebab Case" ], + "vs/editor/contrib/linkedEditing/browser/linkedEditing": [ + "Start Linked Editing", + "Background color when the editor auto renames on type." + ], + "vs/editor/contrib/links/browser/links": [ + "Failed to open this link because it is not well-formed: {0}", + "Failed to open this link because its target is missing.", + "Execute command", + "Follow link", + "cmd + click", + "ctrl + click", + "option + click", + "alt + click", + "Execute command {0}", + "Open Link" + ], "vs/editor/contrib/multicursor/browser/multicursor": [ "Cursor added: {0}", "Cursors added: {0}", @@ -22153,7 +24011,9 @@ "Rename failed to apply edits", "Rename failed to compute edits", "Rename Symbol", - "Enable/disable the ability to preview changes before renaming" + "Enable/disable the ability to preview changes before renaming", + "Focus Next Rename Suggestion", + "Focus Previous Rename Suggestion" ], "vs/editor/contrib/smartSelect/browser/smartSelect": [ "Expand Selection", @@ -22183,22 +24043,17 @@ "Developer: Force Retokenize" ], "vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode": [ - "Toggle Tab Key Moves Focus", "Pressing Tab will now move focus to the next focusable element", - "Pressing Tab will now insert the tab character" - ], - "vs/editor/contrib/unusualLineTerminators/browser/unusualLineTerminators": [ - "Unusual Line Terminators", - "Detected unusual line terminators", - "The file '{0}' contains one or more unusual line terminator characters, like Line Separator (LS) or Paragraph Separator (PS).\n\nIt is recommended to remove them from the file. This can be configured via `editor.unusualLineTerminators`.", - "&&Remove Unusual Line Terminators", - "Ignore" + "Pressing Tab will now insert the tab character", + "Toggle Tab Key Moves Focus", + "Determines whether the tab key moves focus around the workbench or inserts the tab character in the current editor. This is also called tab trapping, tab navigation, or tab focus mode." ], "vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter": [ "Icon shown with a warning message in the extensions editor.", "This document contains many non-basic ASCII unicode characters", "This document contains many ambiguous unicode characters", "This document contains many invisible unicode characters", + "Configure Unicode Highlight Options", "The character {0} could be confused with the ASCII character {1}, which is more common in source code.", "The character {0} could be confused with the character {1}, which is more common in source code.", "The character {0} is invisible.", @@ -22217,17 +24072,23 @@ "Show Exclude Options", "Exclude {0} (invisible character) from being highlighted", "Exclude {0} from being highlighted", - "Allow unicode characters that are more common in the language \"{0}\".", - "Configure Unicode Highlight Options" + "Allow unicode characters that are more common in the language \"{0}\"." + ], + "vs/editor/contrib/unusualLineTerminators/browser/unusualLineTerminators": [ + "Unusual Line Terminators", + "Detected unusual line terminators", + "The file '{0}' contains one or more unusual line terminator characters, like Line Separator (LS) or Paragraph Separator (PS).\n\nIt is recommended to remove them from the file. This can be configured via `editor.unusualLineTerminators`.", + "&&Remove Unusual Line Terminators", + "Ignore" + ], + "vs/editor/contrib/wordOperations/browser/wordOperations": [ + "Delete Word" ], "vs/editor/contrib/wordHighlighter/browser/wordHighlighter": [ "Go to Next Symbol Highlight", "Go to Previous Symbol Highlight", "Trigger Symbol Highlight" ], - "vs/editor/contrib/wordOperations/browser/wordOperations": [ - "Delete Word" - ], "vs/editor/contrib/readOnlyMessage/browser/contribution": [ "Cannot edit in read-only input", "Cannot edit in read-only editor" @@ -22252,6 +24113,12 @@ "Pressing Tab in the current editor will insert the tab character. Toggle this behavior {0}.", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding.", "Show Accessibility Help", + "Run the command: List Signal Sounds for an overview of all sounds and their current status.", + "Run the command: List Signal Announcements for an overview of announcements and their current status.", + "Toggle quick chat ({0}) to open or close a chat session.", + "Toggle quick chat is not currently triggerable by a keybinding.", + "Start inline chat ({0}) to create an in editor chat session.", + "The command: Start inline chat is not currentlyt riggerable by a keybinding.", "Developer: Inspect Tokens", "Go to Line/Column...", "Show all Quick Access Providers", @@ -22273,7 +24140,10 @@ "'configuration.jsonValidation.url' must be a URL or relative path", "Expected `contributes.{0}.url` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", "'configuration.jsonValidation.url' is an invalid relative URL: {0}", - "'configuration.jsonValidation.url' must be an absolute URL or start with './' to reference schemas located in the extension." + "'configuration.jsonValidation.url' must be an absolute URL or start with './' to reference schemas located in the extension.", + "File Match", + "Schema", + "JSON Validation" ], "vs/workbench/services/themes/common/colorExtensionPoint": [ "Contributes extension defined themable colors", @@ -22291,7 +24161,13 @@ "'configuration.colors.description' must be defined and can not be empty", "'configuration.colors.defaults' must be defined and must contain 'light' and 'dark'", "If defined, 'configuration.colors.defaults.highContrast' must be a string.", - "If defined, 'configuration.colors.defaults.highContrastLight' must be a string." + "If defined, 'configuration.colors.defaults.highContrastLight' must be a string.", + "ID", + "Description", + "Dark Default", + "Light Default", + "High Contrast Default", + "Colors" ], "vs/workbench/services/themes/common/iconExtensionPoint": [ "Contributes extension defined themable icons", @@ -22334,7 +24210,7 @@ "'configuration.semanticTokenScopes.scopes' values must be an array of strings", "configuration.semanticTokenScopes.scopes': Problems parsing selector {0}." ], - "vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint": [ + "vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint": [ "Errors parsing {0}: {1}", "{0}: Invalid format, JSON object expected.", "The opening bracket character or string sequence.", @@ -22413,6 +24289,9 @@ "Contributes items to the status bar.", "Invalid status bar item contribution." ], + "vs/workbench/api/browser/mainThreadLanguageModels": [ + "Language Models" + ], "vs/workbench/api/browser/mainThreadCLICommands": [ "Cannot install the '{0}' extension because it is declared to not run in this setup." ], @@ -22443,7 +24322,7 @@ "&&OK", "Show &&Preview", "Skip Changes", - "Don't ask again", + "Do not ask me again", "Running 'File Create' participants...", "Running 'File Rename' participants...", "Running 'File Copy' participants...", @@ -22452,7 +24331,6 @@ "Reset choice for 'File operation needs preview'" ], "vs/workbench/api/browser/mainThreadMessageService": [ - "{0} (Extension)", "Extension", "Manage Extension", "Cancel", @@ -22492,31 +24370,28 @@ "Use Port {0} as Sudo..." ], "vs/workbench/api/browser/mainThreadAuthentication": [ - "This account has not been used by any extensions.", - "Cancel", - "Last used this account {0}", - "Has not used this account", - "Manage Trusted Extensions", - "Choose which extensions can access this account", - "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", - "Sign out of '{0}'?", - "&&Sign Out", "Successfully signed out.", "The extension '{0}' wants you to sign in again using {1}.", "The extension '{0}' wants to sign in using {1}.", - "&&Allow" + "&&Allow", + "Learn more" + ], + "vs/workbench/browser/parts/titlebar/windowTitle": [ + "[Administrator]", + "[Superuser]", + "[Extension Development Host]" ], "vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions": [ "Icon to toggle the auxiliary bar off in its right position.", "Icon to toggle the auxiliary bar on in its right position.", "Icon to toggle the auxiliary bar in its left position.", "Icon to toggle the auxiliary bar on in its left position.", - "Toggle Secondary Side Bar Visibility", "Secondary Side Bar", "Secondary Si&&de Bar", - "Focus into Secondary Side Bar", "Toggle Secondary Side Bar", "Toggle Secondary Side Bar", + "Toggle Secondary Side Bar Visibility", + "Focus into Secondary Side Bar", "Hide Secondary Side Bar" ], "vs/workbench/browser/parts/panel/panelActions": [ @@ -22525,36 +24400,36 @@ "Icon to close a panel.", "Icon to toggle the panel off when it is on.", "Icon to toggle the panel on when it is off.", - "Toggle Panel Visibility", "Panel", "&&Panel", "Focus into Panel", - "Focus into Panel", - "Move Panel Left", "Left", - "Move Panel Right", "Right", - "Move Panel To Bottom", "Bottom", - "Set Panel Alignment to Left", "Left", - "Set Panel Alignment to Right", "Right", - "Set Panel Alignment to Center", "Center", - "Set Panel Alignment to Justify", "Justify", "Panel Position", "Align Panel", - "Previous Panel View", - "Next Panel View", - "Toggle Maximized Panel", "Maximize Panel Size", "Restore Panel Size", "Maximizing the panel is only supported when it is center aligned.", - "Close Panel", - "Close Secondary Side Bar", "Toggle Panel", + "Toggle Panel Visibility", + "Focus into Panel", + "Move Panel Left", + "Move Panel Right", + "Move Panel To Bottom", + "Set Panel Alignment to Left", + "Set Panel Alignment to Right", + "Set Panel Alignment to Center", + "Set Panel Alignment to Justify", + "Previous Panel View", + "Next Panel View", + "Toggle Maximized Panel", + "Hide Panel", + "Hide Secondary Side Bar", "Hide Panel", "Move Panel Views To Secondary Side Bar", "Move Panel Views To Secondary Side Bar", @@ -22607,6 +24482,7 @@ "An activation event emitted when a specific terminal profile is launched.", "An activation event emitted when a command matches the selector associated with this ID", "An activation event emitted when a specified walkthrough is opened.", + "An activation event emitted when the issue reporter is opened.", "An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.", "Array of badges to display in the sidebar of the Marketplace's extension page.", "Badge image URL.", @@ -22645,6 +24521,23 @@ "The pricing information for the extension. Can be Free (default) or Trial. For more details visit: https://code.visualstudio.com/api/working-with-extensions/publishing-extension#extension-pricing-label", "API proposals that the respective extensions can freely use." ], + "vs/workbench/browser/parts/views/treeView": [ + "There is no data provider registered that can provide view data.", + "Whether the the tree view with id {0} enables collapse all.", + "Whether the tree view with id {0} enables refresh.", + "Refresh", + "Collapse All", + "Whether collapse all is toggled for the tree view with id {0}.", + "Error running command {1}: {0}. This is likely caused by the extension that contributes {1}." + ], + "vs/workbench/browser/parts/views/viewPaneContainer": [ + "Views", + "Move View Up", + "Move View Left", + "Move View Down", + "Move View Right", + "Move Views" + ], "vs/workbench/contrib/debug/common/debug": [ "Debug type of the active debug session. For example 'python'.", "Debug type of the selected launch configuration. For example 'python'.", @@ -22668,10 +24561,13 @@ "Represents the item type of the focused element in the WATCH view. For example: 'expression', 'variable'", "Indicates whether the item in the view has an associated memory refrence.", "Represents the item type of the focused element in the BREAKPOINTS view. For example: 'breakpoint', 'exceptionBreakppint', 'functionBreakpoint', 'dataBreakpoint'", + "Whether the breakpoint item is a data breakpoint on a byte range.", + "Whether the breakpoint has multiple modes it can switch to.", "True when the focused breakpoint supports conditions.", "True when the focused sessions supports the LOADED SCRIPTS view", "Represents the item type of the focused element in the LOADED SCRIPTS view.", "True when the focused session is 'attach'.", + "True when the focused session is run without debugging.", "True when the focused session supports 'stepBack' requests.", "True when the focused session supports 'restartFrame' requests.", "True when the focused stack frame suppots 'restartFrame'.", @@ -22682,6 +24578,7 @@ "True when there is at least one debug extension installed and enabled.", "Represents the context the debug adapter sets on the focused variable in the VARIABLES view.", "True when the focused session supports 'setVariable' request.", + "True when the focused session supports 'getBreakpointInfo' request on an address.", "True when the focused session supports 'setExpression' request.", "True when the focused session supports to break when value changes.", "True when the focused breakpoint supports to break when value is accessed.", @@ -22690,6 +24587,12 @@ "True when the focused session supports the suspend debuggee capability.", "True when the focused variable has an 'evalauteName' field set.", "True when the focused variable is read-only.", + "Value of the variable, present for debug visualization clauses.", + "Type of the variable, present for debug visualization clauses.", + "Any interfaces or contracts that the variable satisfies, present for debug visualization clauses.", + "Name of the variable, present for debug visualization clauses.", + "Language of the variable source, present for debug visualization clauses.", + "Extension ID of the variable source, present for debug visualization clauses.", "True when the exception widget is visible.", "True when there is more than 1 debug console.", "True when there is more than 1 active debug session.", @@ -22716,37 +24619,23 @@ "True when the focus is inside a compact item's last part in the EXPLORER view.", "True when a workspace in the EXPLORER view has some collapsible root child." ], - "vs/workbench/browser/parts/views/viewPaneContainer": [ - "Views", - "Move View Up", - "Move View Left", - "Move View Down", - "Move View Right", - "Move Views" - ], "vs/workbench/contrib/remote/browser/remoteExplorer": [ "No forwarded ports. Forward a port to access your running services locally.\n[Forward a Port]({0})", "No forwarded ports. Forward a port to access your locally running services over the internet.\n[Forward a Port]({0})", - "Ports", "1 forwarded port", "{0} forwarded ports", "No Ports Forwarded", "Forwarded Ports: {0}", "Forwarded Ports", - "Your application running on port {0} is available. ", + "Over 20 ports have been automatically forwarded. The `process` based automatic port forwarding has been switched to `hybrid` in settings. Some ports may no longer be detected.", + "Undo", + "Show Setting", + "Your application{0} running on port {1} is available. ", "[See all forwarded ports]({0})", "You'll need to run as superuser to use port {0} locally. ", "Make Public", - "Use Port {0} as Sudo..." - ], - "vs/workbench/browser/parts/views/treeView": [ - "There is no data provider registered that can provide view data.", - "Whether the the tree view with id {0} enables collapse all.", - "Whether the tree view with id {0} enables refresh.", - "Refresh", - "Collapse All", - "Whether collapse all is toggled for the tree view with id {0}.", - "Error running command {1}: {0}. This is likely caused by the extension that contributes {1}." + "Use Port {0} as Sudo...", + "Ports" ], "vs/workbench/common/editor/sideBySideEditorInput": [ "{0} - {1}" @@ -22801,7 +24690,6 @@ "Current Problem", "Current Problem", "Search Marketplace Extensions for '{0}'...", - "Change Language Mode", "No text editor active at this time", "({0}) - Configured Language", "({0})", @@ -22812,11 +24700,9 @@ "Select Language Mode", "Current Association", "Select Language Mode to Associate with '{0}'", - "Change End of Line Sequence", "No text editor active at this time", "The active code editor is read-only.", "Select End of Line Sequence", - "Change File Encoding", "No text editor active at this time", "No text editor active at this time", "No file active at this time", @@ -22825,16 +24711,73 @@ "Select Action", "Guessed from content", "Select File Encoding to Reopen File", - "Select File Encoding to Save with" + "Select File Encoding to Save with", + "Change Language Mode", + "Change End of Line Sequence", + "Change File Encoding" + ], + "vs/workbench/browser/parts/editor/diffEditorCommands": [ + "Compare", + "Compare", + "Go to Next Change", + "Go to Previous Change", + "Toggle Inline View", + "Swap Left and Right Editor Side" + ], + "vs/workbench/browser/parts/editor/editorCommands": [ + "Move the active editor by tabs or groups", + "Active editor move argument", + "Argument Properties:\n\t* 'to': String value providing where to move.\n\t* 'by': String value providing the unit for move (by tab or by group).\n\t* 'value': Number value providing how many positions or an absolute position to move.", + "Copy the active editor by groups", + "Active editor copy argument", + "Argument Properties:\n\t* 'to': String value providing where to copy.\n\t* 'value': Number value providing how many positions or an absolute position to copy.", + "Split Editor in Group", + "Join Editor in Group", + "Toggle Split Editor in Group", + "Toggle Layout of Split Editor in Group", + "Focus First Side in Active Editor", + "Focus Second Side in Active Editor", + "Focus Other Side in Active Editor", + "Toggle Editor Group Lock", + "Lock Editor Group", + "Unlock Editor Group" + ], + "vs/editor/browser/editorExtensions": [ + "&&Undo", + "Undo", + "&&Redo", + "Redo", + "&&Select All", + "Select All" ], "vs/workbench/browser/parts/editor/editorActions": [ + "Split Editor Up", + "Split Editor Down", + "Close Editor", + "Unpin Editor", + "Close", + "Go Forward", + "&&Forward", + "Go Back", + "&&Back", + "Do you want to clear all recently opened files and workspaces?", + "This action is irreversible!", + "&&Clear", + "Do you want to clear the history of recently opened editors?", + "This action is irreversible!", + "&&Clear", + "Split Editor into Left Group", + "&&Move Editor into New Window", + "&&Copy Editor into New Window", + "&&Move Editor Group into New Window", + "&&Copy Editor Group into New Window", + "&&Restore Editors into Main Window", + "&&New Empty Editor Window", "Split Editor", "Split Editor Orthogonal", "Split Editor Left", "Split Editor Right", "Split Editor Up", - "Split Editor Up", - "Split Editor Down", "Split Editor Down", "Join Editor Group with Next Group", "Join All Editor Groups", @@ -22848,9 +24791,6 @@ "Focus Right Editor Group", "Focus Editor Group Above", "Focus Editor Group Below", - "Close Editor", - "Unpin Editor", - "Close", "Revert and Close Editor", "Close Editors to the Left in Group", "Close All Editors", @@ -22865,10 +24805,12 @@ "Duplicate Editor Group Right", "Duplicate Editor Group Up", "Duplicate Editor Group Down", - "Maximize Editor Group", + "Expand Editor Group", + "Expand Editor Group and Hide Side Bars", "Reset Editor Group Sizes", "Toggle Editor Group Sizes", "Maximize Editor Group and Hide Side Bars", + "Toggle Maximize Editor Group", "Open Next Editor", "Open Previous Editor", "Open Next Editor in Group", @@ -22876,11 +24818,7 @@ "Open First Editor in Group", "Open Last Editor in Group", "Go Forward", - "Go Forward", - "&&Forward", "Go Back", - "Go Back", - "&&Back", "Go Previous", "Go Forward in Edit Locations", "Go Back in Edit Locations", @@ -22891,10 +24829,7 @@ "Go Previous in Navigation Locations", "Go to Last Navigation Location", "Reopen Closed Editor", - "Clear Recently Opened", - "Do you want to clear all recently opened files and workspaces?", - "This action is irreversible!", - "&&Clear", + "Clear Recently Opened...", "Show Editors in Active Group By Most Recently Used", "Show All Editors By Appearance", "Show All Editors By Most Recently Used", @@ -22908,9 +24843,6 @@ "Open Next Recently Used Editor In Group", "Open Previous Recently Used Editor In Group", "Clear Editor History", - "Do you want to clear the history of recently opened editors?", - "This action is irreversible!", - "&&Clear", "Move Editor Left", "Move Editor Right", "Move Editor into Previous Group", @@ -22926,7 +24858,6 @@ "Split Editor into Group Above", "Split Editor into Group Below", "Split Editor into Left Group", - "Split Editor into Left Group", "Split Editor into Right Group", "Split Editor into First Group", "Split Editor into Last Group", @@ -22943,102 +24874,55 @@ "New Editor Group Above", "New Editor Group Below", "Toggle Editor Type", - "Reopen Editor With Text Editor" - ], - "vs/editor/browser/editorExtensions": [ - "&&Undo", - "Undo", - "&&Redo", - "Redo", - "&&Select All", - "Select All" - ], - "vs/workbench/browser/parts/editor/editorCommands": [ - "Move the active editor by tabs or groups", - "Active editor move argument", - "Argument Properties:\n\t* 'to': String value providing where to move.\n\t* 'by': String value providing the unit for move (by tab or by group).\n\t* 'value': Number value providing how many positions or an absolute position to move.", - "Copy the active editor by groups", - "Active editor copy argument", - "Argument Properties:\n\t* 'to': String value providing where to copy.\n\t* 'value': Number value providing how many positions or an absolute position to copy.", - "Go to Next Change", - "Go to Previous Change", - "Toggle Inline View", - "Compare", - "Split Editor in Group", - "Join Editor in Group", - "Toggle Split Editor in Group", - "Toggle Layout of Split Editor in Group", - "Focus First Side in Active Editor", - "Focus Second Side in Active Editor", - "Focus Other Side in Active Editor", - "Toggle Editor Group Lock", - "Lock Editor Group", - "Unlock Editor Group" - ], - "vs/workbench/browser/parts/editor/editorQuickAccess": [ - "No matching editors", - "{0}, unsaved changes, {1}", - "{0}, {1}", - "{0}, unsaved changes", - "Close Editor" + "Reopen Editor With Text Editor", + "Move Editor into New Window", + "Copy Editor into New Window", + "Move Editor Group into New Window", + "Copy Editor Group into New Window", + "Restore Editors into Main Window", + "New Empty Editor Window" ], "vs/workbench/browser/parts/editor/editorConfiguration": [ "Interactive Window", "Markdown Preview", + "Simple Browser", + "Live Preview", "If an editor matching one of the listed types is opened as the first in an editor group and more than one group is open, the group is automatically locked. Locked groups will only be used for opening editors when explicitly chosen by a user gesture (for example drag and drop), but not by default. Consequently, the active editor in a locked group is less likely to be replaced accidentally with a different editor.", "The default editor for files detected as binary. If undefined, the user will be presented with a picker.", "Configure [glob patterns](https://aka.ms/vscode-glob-patterns) to editors (for example `\"*.hex\": \"hexEditor.hexedit\"`). These have precedence over the default behavior.", "Controls the minimum size of a file in MB before asking for confirmation when opening in the editor. Note that this setting may not apply to all editor types and environments." ], + "vs/workbench/browser/parts/editor/editorQuickAccess": [ + "No matching editors", + "{0}, unsaved changes, {1}", + "{0}, {1}", + "{0}, unsaved changes", + "Close Editor" + ], "vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart": [ + "Activity Bar Position", "Move Secondary Side Bar Left", "Move Secondary Side Bar Right", "Hide Secondary Side Bar" ], - "vs/workbench/browser/parts/activitybar/activitybarPart": [ - "Accounts icon in the view bar.", - "Menu", - "Hide Menu", - "Accounts", - "Hide Activity Bar", - "Reset Location", - "Reset Location", - "Manage", - "Accounts", - "Manage", - "Accounts" - ], "vs/workbench/browser/parts/panel/panelPart": [ - "Reset Location", - "Reset Location", - "Drag a view here to display.", - "More Actions...", "Panel Position", "Align Panel", "Hide Panel" ], - "vs/workbench/browser/parts/editor/editorGroupView": [ - "Empty editor group actions", - "{0} (empty)", - "Group {0}", - "Editor Group {0}" - ], - "vs/workbench/browser/parts/editor/editorDropTarget": [ - "Hold __{0}__ to drop into editor" - ], - "vs/workbench/browser/parts/statusbar/statusbarActions": [ - "Hide '{0}'", - "Focus Status Bar" + "vs/workbench/browser/parts/sidebar/sidebarPart": [ + "Toggle Activity Bar Visibility" ], "vs/platform/actions/common/menuResetAction": [ "Reset All Menus" ], "vs/platform/actions/common/menuService": [ - "Hide '{0}'" + "Hide '{0}'", + "Configure Keybinding" ], - "vs/base/browser/ui/icons/iconSelectBox": [ - "Search icons", - "No results" + "vs/workbench/browser/parts/statusbar/statusbarActions": [ + "Hide '{0}'", + "Focus Status Bar" ], "vs/base/browser/ui/dialog/dialog": [ "OK", @@ -23049,21 +24933,23 @@ "Close Dialog" ], "vs/workbench/services/preferences/browser/keybindingsEditorInput": [ + "Icon of the keybindings editor label.", "Keyboard Shortcuts" ], "vs/workbench/services/preferences/common/preferencesEditorInput": [ + "Icon of the settings editor label.", "Settings" ], "vs/workbench/services/preferences/common/preferencesModels": [ "Commonly Used", "Override key bindings by placing them into your key bindings file." ], - "vs/workbench/services/editor/common/editorResolverService": [ - "Configure [glob patterns](https://aka.ms/vscode-glob-patterns) to editors (for example `\"*.hex\": \"hexEditor.hexedit\"`). These have precedence over the default behavior." - ], "vs/workbench/services/textfile/common/textFileEditorModel": [ "File Encoding Changed" ], + "vs/workbench/services/editor/common/editorResolverService": [ + "Configure [glob patterns](https://aka.ms/vscode-glob-patterns) to editors (for example `\"*.hex\": \"hexEditor.hexedit\"`). These have precedence over the default behavior." + ], "vs/base/common/keybindingLabels": [ "Ctrl", "Shift", @@ -23086,10 +24972,32 @@ "Alt", "Super" ], + "vs/platform/keybinding/common/abstractKeybindingService": [ + "({0}) was pressed. Waiting for second key of chord...", + "({0}) was pressed. Waiting for next key of chord...", + "The key combination ({0}, {1}) is not a command.", + "The key combination ({0}, {1}) is not a command." + ], + "vs/workbench/services/themes/common/colorThemeData": [ + "Problems parsing JSON theme file: {0}", + "Invalid format for JSON theme file: Object expected.", + "Problem parsing color theme file: {0}. Property 'colors' is not of type 'object'.", + "Problem parsing color theme file: {0}. Property 'tokenColors' should be either an array specifying colors or a path to a TextMate theme file", + "Problem parsing color theme file: {0}. Property 'semanticTokenColors' contains a invalid selector", + "Problem parsing tmTheme file: {0}. 'settings' is not array.", + "Problems parsing tmTheme file: {0}", + "Problems loading tmTheme file {0}: {1}" + ], "vs/workbench/services/themes/common/fileIconThemeSchema": [ "The folder icon for expanded folders. The expanded folder icon is optional. If not set, the icon defined for folder will be shown.", "The folder icon for collapsed folders, and if folderExpanded is not set, also for expanded folders.", "The default file icon, shown for all files that don't match any extension, filename or language id.", + "The folder icon for collapsed root folders, and if rootFolderExpanded is not set, also for expanded root folders.", + "The folder icon for expanded root folders. The expanded root folder icon is optional. If not set, the icon defined for root folder will be shown.", + "Associates root folder names to icons. The object key is the root folder name. No patterns or wildcards are allowed. Root folder name matching is case insensitive.", + "The ID of the icon definition for the association.", + "Associates root folder names to icons for expanded root folders. The object key is the root folder name. No patterns or wildcards are allowed. Root folder name matching is case insensitive.", + "The ID of the icon definition for the association.", "Associates folder names to icons. The object key is the folder name, not including any path segments. No patterns or wildcards are allowed. Folder name matching is case insensitive.", "The ID of the icon definition for the association.", "Associates folder names to icons for expanded folders. The object key is the folder name, not including any path segments. No patterns or wildcards are allowed. Folder name matching is case insensitive.", @@ -23125,22 +25033,6 @@ "Problems parsing file icons file: {0}", "Invalid format for file icons theme file: Object expected." ], - "vs/workbench/services/themes/common/colorThemeData": [ - "Problems parsing JSON theme file: {0}", - "Invalid format for JSON theme file: Object expected.", - "Problem parsing color theme file: {0}. Property 'colors' is not of type 'object'.", - "Problem parsing color theme file: {0}. Property 'tokenColors' should be either an array specifying colors or a path to a TextMate theme file", - "Problem parsing color theme file: {0}. Property 'semanticTokenColors' contains a invalid selector", - "Problem parsing tmTheme file: {0}. 'settings' is not array.", - "Problems parsing tmTheme file: {0}", - "Problems loading tmTheme file {0}: {1}" - ], - "vs/platform/keybinding/common/abstractKeybindingService": [ - "({0}) was pressed. Waiting for second key of chord...", - "({0}) was pressed. Waiting for next key of chord...", - "The key combination ({0}, {1}) is not a command.", - "The key combination ({0}, {1}) is not a command." - ], "vs/workbench/services/themes/common/colorThemeSchema": [ "Colors and styles for the token.", "Foreground color for the token.", @@ -23156,25 +25048,6 @@ "Whether semantic highlighting should be enabled for this theme.", "Colors for semantic tokens" ], - "vs/workbench/services/themes/common/themeExtensionPoints": [ - "Contributes textmate color themes.", - "Id of the color theme as used in the user settings.", - "Label of the color theme as shown in the UI.", - "Base theme defining the colors around the editor: 'vs' is the light color theme, 'vs-dark' is the dark color theme. 'hc-black' is the dark high contrast theme, 'hc-light' is the light high contrast theme.", - "Path of the tmTheme file. The path is relative to the extension folder and is typically './colorthemes/awesome-color-theme.json'.", - "Contributes file icon themes.", - "Id of the file icon theme as used in the user settings.", - "Label of the file icon theme as shown in the UI.", - "Path of the file icon theme definition file. The path is relative to the extension folder and is typically './fileicons/awesome-icon-theme.json'.", - "Contributes product icon themes.", - "Id of the product icon theme as used in the user settings.", - "Label of the product icon theme as shown in the UI.", - "Path of the product icon theme definition file. The path is relative to the extension folder and is typically './producticons/awesome-product-icon-theme.json'.", - "Extension point `{0}` must be an array.", - "Expected string in `contributes.{0}.path`. Provided value: {1}", - "Expected string in `contributes.{0}.id`. Provided value: {1}", - "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable." - ], "vs/workbench/services/themes/browser/productIconThemeData": [ "Problems processing product icons definitions in {0}:\n{1}", "Default", @@ -23199,54 +25072,37 @@ "The style of the font. See https://developer.mozilla.org/en-US/docs/Web/CSS/font-style for valid values.", "Association of icon name to a font character." ], - "vs/workbench/services/themes/common/themeConfiguration": [ - "Specifies the color theme used in the workbench.", - "Theme is unknown or not installed.", - "Specifies the preferred color theme for dark OS appearance when {0} is enabled.", - "Theme is unknown or not installed.", - "Specifies the preferred color theme for light OS appearance when {0} is enabled.", - "Theme is unknown or not installed.", - "Specifies the preferred color theme used in high contrast dark mode when {0} is enabled.", - "Theme is unknown or not installed.", - "Specifies the preferred color theme used in high contrast light mode when {0} is enabled.", - "Theme is unknown or not installed.", - "If set, automatically switch to the preferred color theme based on the OS appearance. If the OS appearance is dark, the theme specified at {0} is used, for light {1}.", - "Overrides colors from the currently selected color theme.", - "Specifies the file icon theme used in the workbench or 'null' to not show any file icons.", - "None", - "No file icons", - "File icon theme is unknown or not installed.", - "Specifies the product icon theme used.", - "Default", - "Default", - "Product icon theme is unknown or not installed.", - "If enabled, will automatically change to high contrast theme if the OS is using a high contrast theme. The high contrast theme to use is specified by {0} and {1}", - "Sets the colors and styles for comments", - "Sets the colors and styles for strings literals.", - "Sets the colors and styles for keywords.", - "Sets the colors and styles for number literals.", - "Sets the colors and styles for type declarations and references.", - "Sets the colors and styles for functions declarations and references.", - "Sets the colors and styles for variables declarations and references.", - "Sets colors and styles using textmate theming rules (advanced).", - "Whether semantic highlighting should be enabled for this theme.", - "Use `enabled` in `editor.semanticTokenColorCustomizations` setting instead.", - "Use `enabled` in {0} setting instead.", - "Overrides editor syntax colors and font style from the currently selected color theme.", - "Whether semantic highlighting is enabled or disabled for this theme", - "Semantic token styling rules for this theme.", - "Overrides editor semantic token color and styles from the currently selected color theme." + "vs/workbench/services/themes/common/themeExtensionPoints": [ + "Contributes textmate color themes.", + "Id of the color theme as used in the user settings.", + "Label of the color theme as shown in the UI.", + "Base theme defining the colors around the editor: 'vs' is the light color theme, 'vs-dark' is the dark color theme. 'hc-black' is the dark high contrast theme, 'hc-light' is the light high contrast theme.", + "Path of the tmTheme file. The path is relative to the extension folder and is typically './colorthemes/awesome-color-theme.json'.", + "Contributes file icon themes.", + "Id of the file icon theme as used in the user settings.", + "Label of the file icon theme as shown in the UI.", + "Path of the file icon theme definition file. The path is relative to the extension folder and is typically './fileicons/awesome-icon-theme.json'.", + "Contributes product icon themes.", + "Id of the product icon theme as used in the user settings.", + "Label of the product icon theme as shown in the UI.", + "Path of the product icon theme definition file. The path is relative to the extension folder and is typically './producticons/awesome-product-icon-theme.json'.", + "Color Themes", + "File Icon Themes", + "Product Icon Themes", + "Themes", + "Extension point `{0}` must be an array.", + "Expected string in `contributes.{0}.path`. Provided value: {1}", + "Expected string in `contributes.{0}.id`. Provided value: {1}", + "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable." ], "vs/workbench/services/extensionManagement/browser/extensionBisect": [ "I can't reproduce", "I can reproduce", "Extension Bisect is active and has disabled 1 extension. Check if you can still reproduce the problem and proceed by selecting from these options.", "Extension Bisect is active and has disabled {0} extensions. Check if you can still reproduce the problem and proceed by selecting from these options.", - "Start Extension Bisect", "Extension Bisect", "Extension Bisect will use binary search to find an extension that causes a problem. During the process the window reloads repeatedly (~{0} times). Each time you must confirm if you are still seeing problems.", "&&Start Extension Bisect", - "Continue Extension Bisect", "Extension Bisect", "Extension Bisect is done but no extension has been identified. This might be a problem with {0}.", "Extension Bisect", @@ -23260,6 +25116,8 @@ "I can &&reproduce", "&&Stop Bisect", "&&Cancel Bisect", + "Start Extension Bisect", + "Continue Extension Bisect", "Stop Extension Bisect" ], "vs/workbench/services/userDataProfile/browser/settingsResource": [ @@ -23272,8 +25130,8 @@ "Snippets", "Select Snippet {0}" ], - "vs/workbench/services/userDataProfile/browser/globalStateResource": [ - "UI State" + "vs/workbench/services/userDataProfile/browser/tasksResource": [ + "User Tasks" ], "vs/workbench/services/userDataProfile/browser/extensionsResource": [ "Extensions", @@ -23281,8 +25139,8 @@ "Select {0} Extension", "Select {0} Extension" ], - "vs/workbench/services/userDataProfile/browser/tasksResource": [ - "User Tasks" + "vs/workbench/services/userDataProfile/browser/globalStateResource": [ + "UI State" ], "vs/workbench/services/userDataProfile/common/userDataProfileIcons": [ "Settings icon in the view bar." @@ -23298,11 +25156,14 @@ "Saving '{0}'" ], "vs/workbench/services/views/common/viewContainerModel": [ - "Views" + "Show Views Log" ], - "vs/workbench/services/hover/browser/hoverWidget": [ + "vs/editor/browser/services/hoverService/hoverWidget": [ "Hold {0} key to mouse over" ], + "vs/editor/browser/services/hoverService/updatableHoverWidget": [ + "Loading..." + ], "vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl": [ "Already Logging.", "Stop", @@ -23316,6 +25177,12 @@ "Invalid value in `contributes.{0}.tokenTypes`. Must be an object map from scope name to token type. Provided value: {1}", "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable." ], + "vs/workbench/contrib/preferences/browser/keybindingWidgets": [ + "Press desired key combination and then press ENTER.", + "1 existing command has this keybinding", + "{0} existing commands have this keybinding", + "chord to" + ], "vs/editor/contrib/suggest/browser/suggest": [ "Whether any suggestion is focused", "Whether suggestion details are visible", @@ -23326,6 +25193,11 @@ "Whether the default behaviour is to insert or replace", "Whether the current suggestion supports to resolve further details" ], + "vs/workbench/contrib/preferences/browser/preferencesActions": [ + "({0})", + "Select Language", + "Configure Language Specific Settings..." + ], "vs/workbench/contrib/preferences/browser/keybindingsEditor": [ "Record Keys", "Sort by Precedence (Highest first)", @@ -23361,11 +25233,6 @@ "No when context", "use space or enter to change the keybinding." ], - "vs/workbench/contrib/preferences/browser/preferencesActions": [ - "Configure Language Specific Settings...", - "({0})", - "Select Language" - ], "vs/workbench/contrib/preferences/browser/preferencesIcons": [ "Icon for the folder dropdown button in the split JSON Settings editor.", "Icon for the 'more actions' action in the Settings UI.", @@ -23380,6 +25247,13 @@ "Icon for the button that suggests filters for the Settings UI.", "Icon for open settings commands." ], + "vs/workbench/contrib/preferences/common/preferencesContribution": [ + "Split Settings Editor", + "Controls whether to enable the natural language search mode for settings. The natural language search is provided by a Microsoft online service.", + "Hide the Table of Contents while searching.", + "Filter the Table of Contents to just categories that have matching settings. Clicking on a category will filter the results to that category.", + "Controls the behavior of the Settings editor Table of Contents while searching. If this setting is being changed in the Settings editor, the setting will take effect after the search query is modified." + ], "vs/workbench/contrib/preferences/browser/settingsEditor2": [ "Search settings", "Clear Settings Search Input", @@ -23394,84 +25268,43 @@ "Backup and Sync Settings", "Last synced: {0}" ], - "vs/workbench/contrib/preferences/common/preferencesContribution": [ - "Split Settings Editor", - "Controls whether to enable the natural language search mode for settings. The natural language search is provided by a Microsoft online service.", - "Hide the Table of Contents while searching. The search results will not be grouped by category, and instead will be sorted by similarity to the query, with exact keyword matches coming first.", - "Filter the Table of Contents to just categories that have matching settings. Clicking a category will filter the results to that category. The search results will be grouped by category.", - "Controls the behavior of the settings editor Table of Contents while searching." - ], - "vs/workbench/contrib/preferences/browser/keybindingWidgets": [ - "Press desired key combination and then press ENTER.", - "1 existing command has this keybinding", - "{0} existing commands have this keybinding", - "chord to" - ], "vs/workbench/contrib/performance/browser/perfviewEditor": [ "Startup Performance" ], - "vs/workbench/contrib/notebook/browser/notebookEditor": [ - "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", - "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", - "Enable extension for '{0}'", - "Install extension for '{0}'", - "Open As Text", - "Open in Text Editor" - ], - "vs/workbench/contrib/notebook/common/notebookEditorInput": [ - "Notebook '{0}' could not be saved." - ], - "vs/workbench/contrib/notebook/browser/services/notebookServiceImpl": [ - "Install extension for '{0}'" - ], - "vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor": [ - "Notebook Text Diff" - ], - "vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl": [ - "Disable other keymaps ({0}) to avoid conflicts between keybindings?", - "Yes", - "No" - ], - "vs/workbench/contrib/notebook/browser/services/notebookExecutionServiceImpl": [ - "Executing a notebook cell will run code from this workspace." - ], - "vs/editor/common/languages/modesRegistry": [ - "Plain Text" - ], - "vs/workbench/contrib/comments/browser/commentReply": [ - "Reply...", - "Type a new comment", - "Reply...", - "Reply..." - ], - "vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl": [ - "Clear Notebook Kernels MRU Cache" - ], - "vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl": [ - "Notebook rendering" - ], - "vs/workbench/contrib/notebook/browser/notebookAccessibility": [ - "The notebook view is a collection of code and markdown cells. Code cells can be executed and will produce output directly below the cell.", - "The Edit Cell command ({0}) will focus on the cell input.", - "The Edit Cell command will focus on the cell input and is currently not triggerable by a keybinding.", - "The Quit Edit command ({0}) will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.", - "The Quit Edit command will set focus on the cell container and is currently not triggerable by a keybinding.", - "The Focus Output command ({0}) will set focus in the cell's output.", - "The Quit Edit command will set focus in the cell's output and is currently not triggerable by a keybinding.", - "The up and down arrows will move focus between cells while focused on the outer cell container", - "The Execute Cell command ({0}) executes the cell that currently has focus.", - "The Execute Cell command executes the cell that currently has focus and is currently not triggerable by a keybinding.", - "The Insert Cell Above/Below commands will create new empty code cells", - "The Change Cell to Code/Markdown commands are used to switch between cell types." - ], - "vs/workbench/contrib/accessibility/browser/accessibleViewActions": [ - "Show Next in Accessible View", - "Show Previous in Accessible View", - "Go To Symbol in Accessible View", - "Open Accessibility Help", - "Open Accessible View", - "Disable Accessible View Hint", - "Accept Inline Completion" + "vs/workbench/contrib/speech/common/speechService": [ + "A speech provider is registered to the speech service.", + "A speech-to-text session is in progress.", + "Danish (Denmark)", + "German (Germany)", + "English (Australia)", + "English (Canada)", + "English (United Kingdom)", + "English (Ireland)", + "English (India)", + "English (New Zealand)", + "English (United States)", + "Spanish (Spain)", + "Spanish (Mexico)", + "French (Canada)", + "French (France)", + "Hindi (India)", + "Italian (Italy)", + "Japanese (Japan)", + "Korean (South Korea)", + "Dutch (Netherlands)", + "Portuguese (Portugal)", + "Portuguese (Brazil)", + "Russian (Russia)", + "Swedish (Sweden)", + "Turkish (Türkiye)", + "Chinese (Simplified, China)", + "Chinese (Traditional, Hong Kong)", + "Chinese (Traditional, Taiwan)" + ], + "vs/workbench/contrib/speech/browser/speechService": [ + "Contributes a Speech Provider", + "Unique name for this Speech Provider.", + "A description of this Speech Provider, shown in the UI." ], "vs/workbench/contrib/accessibility/browser/accessibleView": [ "({0}) {1}", @@ -23487,365 +25320,204 @@ "Accessibility Help, {0}", "Accessibility Help", "Accessible View", - "Navigate to the toolbar (Shift+Tab)).", + "Navigate to the toolbar (Shift+Tab).", "In the accessible view, you can:\n", + " - Insert the code block at the cursor ({0}).\n", + " - Insert the code block at the cursor by configuring a keybinding for the Chat: Insert Code Block command.\n", + " - Insert the code block into a new file ({0}).\n", + " - Insert the code block into a new file by configuring a keybinding for the Chat: Insert into New File command.\n", + " - Run the code block in the terminal ({0}).\n", + " - Run the coe block in the terminal by configuring a keybinding for the Chat: Insert into Terminal command.\n", "Show the next ({0}) or previous ({1}) item.", "Show the next or previous item by configuring keybindings for the Show Next & Previous in Accessible View commands.", "\n\nDisable accessibility verbosity for this feature ({0}).", "\n\nAdd a keybinding for the command Disable Accessible View Hint, which disables accessibility verbosity for this feature.s", - "Go to a symbol ({0})", + "Go to a symbol ({0}).", "To go to a symbol, configure a keybinding for the command Go To Symbol in Accessible View", "Inspect this in the accessible view with {0}", "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.", "Type to search symbols", "Go to Symbol Accessible View" ], - "vs/workbench/contrib/notebook/browser/controller/coreActions": [ - "Notebook", - "Insert Cell", - "Notebook Cell", - "Share" + "vs/workbench/contrib/accessibility/browser/accessibleViewContributions": [ + "{0} Source: {1}", + "{0}", + "Clear Notification", + "Clear Notification" ], - "vs/workbench/contrib/notebook/browser/controller/insertCellActions": [ - "Insert Code Cell Above", - "Insert Code Cell Above and Focus Container", - "Insert Code Cell Below", - "Insert Code Cell Below and Focus Container", - "Insert Markdown Cell Above", - "Insert Markdown Cell Below", - "Add Code Cell At Top", - "Add Markdown Cell At Top", - "Code", - "Add Code Cell", - "Add Code", - "Add Code Cell", - "Code", - "Add Code Cell", - "Code", - "Add Code Cell", - "Add Code", - "Add Code Cell", - "Markdown", - "Add Markdown Cell", - "Markdown", - "Add Markdown Cell", - "Markdown", - "Add Markdown Cell" + "vs/workbench/contrib/accessibility/browser/accessibleViewActions": [ + "Show Next in Accessible View", + "Accessible View: Next Code Block", + "Accessible View: Previous Code Block", + "Show Previous in Accessible View", + "Go To Symbol in Accessible View", + "Open Accessibility Help", + "Open Accessible View", + "Disable Accessible View Hint", + "Accept Inline Completion" ], - "vs/workbench/contrib/notebook/browser/controller/executeActions": [ - "Render All Markdown Cells", - "Run All", - "Run All", - "Execute Cell", - "Execute Cell", - "Execute Above Cells", - "Execute Cell and Below", - "Execute Cell and Focus Container", - "Execute Cell and Focus Container", - "Stop Cell Execution", - "Stop Cell Execution", - "Execute Notebook Cell and Select Below", - "Execute Notebook Cell and Insert Below", - "Stop Execution", - "Interrupt", - "Go to Running Cell", - "Go to Running Cell", - "Go To", - "Go to Most Recently Failed Cell", - "Go to Most Recently Failed Cell", - "Go To" + "vs/workbench/contrib/chat/browser/actions/chatClearActions": [ + "New Chat", + "New Chat" ], - "vs/workbench/contrib/notebook/browser/controller/cellOutputActions": [ - "Copy Output" + "vs/workbench/contrib/chat/browser/actions/chatActions": [ + "Delete", + "Switch to chat", + "Chat", + "Open Chat", + "Show Chats...", + "Open Editor", + "Clear Input History", + "Clear All Workspace Chats", + "Focus Chat List", + "Focus Chat Input" ], - "vs/workbench/contrib/notebook/browser/controller/layoutActions": [ - "Select between Notebook Layouts", - "Customize Notebook Layout", - "Customize Notebook Layout", - "Customize Notebook...", - "Toggle Notebook Line Numbers", - "Notebook Line Numbers", - "Toggle Cell Toolbar Position", - "Toggle Breadcrumbs", - "Save Mimetype Display Order", - "Settings file to save in", - "User Settings", - "Workspace Settings", - "Reset Notebook Webview" - ], - "vs/workbench/contrib/notebook/browser/controller/editActions": [ - "Edit Cell", - "Stop Editing Cell", - "Delete Cell", - "Delete", - "This cell is running, are you sure you want to delete it?", - "Do not ask me again", - "Clear Cell Outputs", - "Clear All Outputs", - "Change Cell Language", - "Change Cell Language", - "({0}) - Current Language", - "({0})", - "Auto Detect", - "languages (identifier)", - "Select Language Mode", - "Accept Detected Language for Cell", - "Unable to detect cell language" - ], - "vs/workbench/contrib/notebook/browser/controller/foldingController": [ - "Fold Cell", - "Unfold Cell", - "Fold Cell" - ], - "vs/workbench/contrib/notebook/browser/contrib/format/formatting": [ - "Format Notebook", - "Format Notebook", - "Format Cell", - "Format Cells" - ], - "vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted": [ - "Reset notebook getting started" - ], - "vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions": [ - "Toggle Cell Toolbar Position" - ], - "vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard": [ - "Copy Cell", - "Cut Cell", - "Paste Cell", - "Paste Cell Above", - "Toggle Notebook Clipboard Troubleshooting" - ], - "vs/workbench/contrib/notebook/browser/contrib/find/notebookFind": [ - "Hide Find in Notebook", - "Find in Notebook" - ], - "vs/workbench/contrib/notebook/browser/contrib/navigation/arrow": [ - "Focus Next Cell Editor", - "Focus Previous Cell Editor", - "Focus First Cell", - "Focus Last Cell", - "Focus In Active Cell Output", - "Focus Out Active Cell Output", - "Center Active Cell", - "Cell Cursor Page Up", - "Cell Cursor Page Up Select", - "Cell Cursor Page Down", - "Cell Cursor Page Down Select", - "When enabled cursor can navigate to the next/previous cell when the current cursor in the cell editor is at the first/last line." - ], - "vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants": [ - "Formatting", - "Format Notebook", - "Notebook Trim Trailing Whitespace", - "Trim Final New Lines", - "Insert Final New Line", - "Running 'Notebook' code actions", - "Running 'Cell' code actions", - "Getting code actions from '{0}' ([configure]({1})).", - "Applying code action '{0}'." - ], - "vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline": [ - "When enabled notebook outline shows code cells.", - "When enabled notebook breadcrumbs contain code cells.", - "When enabled goto symbol quickpick will display full code symbols from the notebook, as well as markdown headers." - ], - "vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile": [ - "Set Profile" - ], - "vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders": [ - "Unknown cell language. Click to search for '{0}' extensions", - "Select Cell Language Mode", - "Accept Detected Language: {0}" - ], - "vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController": [ - "Success", - "Failed", - "Pending", - "Executing", - "Use the links above to file an issue using the issue reporter.", - "**Last Execution** {0}\n\n**Execution Time** {1}\n\n**Overhead Time** {2}\n\n**Render Times**\n\n{3}" - ], - "vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar": [ - "Notebook Kernel Info", - "{0} (suggestion)", - "Notebook Kernel Selection", - "Select Kernel", - "Select Kernel", - "Notebook Editor Selections", - "Cell {0} ({1} selected)", - "Cell {0} of {1}" - ], - "vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout": [ - "Toggle Layout Troubleshoot", - "Inspect Notebook Layout", - "Clear Notebook Editor Type Cache" - ], - "vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands": [ - "Move Cell Up", - "Move Cell Down", - "Copy Cell Up", - "Copy Cell Down", - "Split Cell", - "Join With Previous Cell", - "Join With Next Cell", - "Join Selected Cells", - "Change Cell to Code", - "Change Cell to Markdown", - "Collapse Cell Input", - "Expand Cell Input", - "Collapse Cell Output", - "Expand Cell Output", - "Toggle Outputs", - "Toggle Outputs", - "Collapse All Cell Inputs", - "Expand All Cell Inputs", - "Collapse All Cell Outputs", - "Expand All Cell Outputs", - "Toggle Scroll Cell Output" - ], - "vs/workbench/contrib/notebook/browser/diff/notebookDiffActions": [ - "Open Text Diff Editor", - "Revert Metadata", - "Switch Output Rendering", - "Revert Outputs", - "Revert Input", - "Show Outputs Differences", - "Show Metadata Differences", - "Show Previous Change", - "Show Next Change", - "Hide Metadata Differences", - "Hide Outputs Differences" - ], - "vs/workbench/contrib/chat/browser/actions/chatActions": [ - "Chat", - "Quick Chat", - "Accept Chat Input", - "Clear Input History", - "Focus Chat List", - "Focus Chat Input", - "Open Editor ({0})", - "Show History", - "Delete", - "Select a chat session to restore" + "vs/workbench/contrib/chat/browser/actions/chatCodeblockActions": [ + "Copy", + "Insert at Cursor", + "Insert into New File", + "Insert into Terminal", + "Next Code Block", + "Previous Code Block", + "Apply Edits" ], "vs/workbench/contrib/chat/browser/actions/chatCopyActions": [ "Copy All", "Copy" ], "vs/workbench/contrib/chat/browser/actions/chatExecuteActions": [ - "Submit", + "Send", + "Submit to Secondary Agent", + "Send to New Chat", "Cancel" ], - "vs/workbench/contrib/chat/browser/actions/chatTitleActions": [ - "Helpful", - "Unhelpful", - "Insert into Notebook", - "Remove Request and Response" + "vs/workbench/contrib/chat/browser/actions/chatFileTreeActions": [ + "Next File Tree", + "Previous File Tree" + ], + "vs/workbench/contrib/chat/browser/actions/chatImportExport": [ + "Chat Session", + "Export Chat...", + "Import Chat..." + ], + "vs/workbench/contrib/chat/browser/actions/chatMoveActions": [ + "Open Chat in Editor", + "Open Chat in New Window", + "Open Chat in Side Bar" ], "vs/workbench/contrib/chat/browser/actions/chatQuickInputActions": [ + "Toggle the quick chat", + "The query to open the quick chat with", + "Whether the query is partial; it will wait for more user input", + "The query to open the quick chat with", "Open in Chat View", "Close Quick Chat", + "Launch Inline Chat", "Quick Chat", - "Open Quick Chat ({0})" + "Open Quick Chat" ], - "vs/workbench/contrib/chat/browser/actions/chatCodeblockActions": [ - "Copy", - "Insert at Cursor", - "Insert Into New File", - "Run in Terminal", - "Next Code Block", - "Previous Code Block" + "vs/workbench/contrib/chat/browser/actions/chatTitleActions": [ + "Rerun...", + "Helpful", + "Unhelpful", + "Report Issue", + "Insert into Notebook", + "Remove Request and Response", + "Rerun Request", + "Rerun without Command Detection" ], - "vs/workbench/contrib/chat/browser/actions/chatImportExport": [ - "Chat Session", - "Export Session", - "Import Session" - ], - "vs/workbench/contrib/chat/browser/chatContributionServiceImpl": [ - "Contributes an Interactive Session provider", - "Unique identifier for this Interactive Session provider.", - "Display name for this Interactive Session provider.", - "An icon for this Interactive Session provider.", - "A condition which must be true to enable this Interactive Session provider.", - "Chat" + "vs/workbench/contrib/chat/browser/chat": [ + "Generating" ], "vs/workbench/contrib/chat/browser/chatEditorInput": [ + "Icon of the chat editor label.", "Chat" ], - "vs/workbench/contrib/chat/common/chatServiceImpl": [ - "Provider returned null response" - ], - "vs/workbench/contrib/chat/browser/actions/chatMoveActions": [ - "Open Session In Editor", - "Open Session In Editor", - "Open Session In Sidebar" - ], "vs/workbench/contrib/chat/common/chatContextKeys": [ - "True when the provider has assigned an id to this response.", "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.", + "When the agent or command was automatically detected", + "True when the current chat response supports issue reporting.", "True when the chat response was filtered out by the server.", "True when the current request is still in progress.", "The chat item is a response.", "The chat item is a request", + "True when the chat text edits have been applied.", "True when the chat input has text.", + "True when the chat input has focus.", "True when focus is in the chat input, false otherwise.", "True when focus is in the chat widget, false otherwise.", - "True when some chat provider has been registered." + "True when chat is enabled because a default chat participant is registered." ], - "vs/workbench/contrib/chat/browser/actions/chatClearActions": [ - "Clear", - "Clear", - "Clear" - ], - "vs/workbench/contrib/chat/common/chatViewModel": [ - "Thinking" - ], - "vs/workbench/contrib/chat/common/chatSlashCommands": [ - "The name of the slash command which will be used as prefix.", - "The details of the slash command.", - "Contributes slash commands to chat", - "Invalid {0}: {1}" - ], - "vs/workbench/contrib/chat/browser/actions/chatFileTreeActions": [ - "Next File Tree", - "Previous File Tree" - ], - "vs/workbench/contrib/accessibility/browser/accessibilityContributions": [ - "{0} Source: {1}", - "{0}", - "Clear Notification", - "Clear Notification" + "vs/workbench/contrib/chat/common/chatServiceImpl": [ + "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date.", + "Show Extension", + "Provider returned null response" ], - "vs/workbench/contrib/chat/common/chatAgents": [ - "The name of the agent which will be used as prefix.", - "The details of the agent.", - "Contributes agents to chat", - "Invalid {0}: {1}" + "vs/workbench/contrib/chat/common/languageModelStats": [ + "Language Models", + "Language models usage statistics of this extension." + ], + "vs/workbench/contrib/chat/browser/chatParticipantContributions": [ + "Contributes a chat participant", + "A unique id for this chat participant.", + "User-facing display name for this chat participant. The user will use '@' with this name to invoke the participant.", + "A description of this chat participant, shown in the UI.", + "**Only** allowed for extensions that have the `defaultChatParticipant` proposal.", + "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message.", + "Commands available for this chat participant, which the user can invoke with a `/`.", + "A short name by which this command is referred to in the UI, e.g. `fix` or * `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant.", + "A description of this command.", + "A condition which must be true to enable this command.", + "When the user clicks this command in `/help`, this text will be submitted to this participant.", + "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message.", + "**Only** allowed for extensions that have the `chatParticipantAdditions` proposal. The names of the variables that are invoked by default", + "Locations in which this chat participant is available.", + "Chat" ], "vs/workbench/contrib/chat/common/chatColors": [ "The border color of a chat request.", + "The background color of a chat request.", "The background color of a chat slash command.", - "The foreground color of a chat slash command." + "The foreground color of a chat slash command.", + "The background color of a chat avatar.", + "The foreground color of a chat avatar." ], "vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib": [ - "Ask a question or type '@' or '/'", - "Ask a question" + "Pick a file" ], "vs/workbench/contrib/inlineChat/browser/inlineChatController": [ - "AI-generated code may be incorrect", - "Getting ready...", "Failed to start editor chat", - "Please consult the error log and try again later.", + "Getting ready...", "AI-generated code may be incorrect", - "Ask a question", - "{0} ({1}, {2} for history)", - "Thinking…", "No results, please refine your input and try again", - "{0}", - "Use tab to navigate to the diff editor and review proposed changes.", "Failed to apply changes.", - "Failed to discard changes." + "Failed to discard changes.", + "Accept or discard changes to continue saving" + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatActions": [ + "Icon which spawns the inline chat from the editor toolbar.", + "Cursor Up", + "Cursor Down", + "Discard", + "Discard...", + "Discard", + "Discard to Clipboard", + "Discard to New File", + "Accept", + "Cancel", + "Close", + "Configure ", + "'{0}' and {1} follow ups ({2})", + "View in Chat", + "Start in Editor", + "Resume Last Dismissed Inline Chat", + "Inline Chat", + "Focus Input", + "Toggle Changes", + "Accept Changes", + "Move to Next Change", + "Move to Previous Change", + "(Developer) Write Exchange to Clipboard" ], "vs/workbench/contrib/inlineChat/common/inlineChat": [ "Whether a provider for interactive editors exists", @@ -23857,7 +25529,6 @@ "Whether the cursor of the iteractive editor input is on the last line", "Whether the cursor of the iteractive editor input is on the start of the input", "Whether the cursor of the iteractive editor input is on the end of the input", - "Whether the interactive editor message is cropped, not cropped or expanded", "Whether the cursor of the outer editor is above or below the interactive editor input", "Whether interactive editor has an active request", "Whether interactive editor has kept a session for quick restore", @@ -23866,7 +25537,10 @@ "Whether interactive editor did change any code", "Whether the user did changes ontop of the inline chat", "The last kind of feedback that was provided", + "Whether the interactive editor supports issue reporting", "Whether the document has changed concurrently", + "Whether the current change supports showing a diff", + "Whether the current change showing a diff", "Background color of the interactive editor widget", "Border color of the interactive editor widget", "Shadow color of the interactive editor widget", @@ -23876,93 +25550,508 @@ "Foreground color of the interactive editor input placeholder", "Background color of the interactive editor input", "Background color of inserted text in the interactive editor input", + "Overview ruler marker color for inline chat inserted content.", + "Overview ruler marker color for inline chat inserted content.", "Background color of removed text in the interactive editor input", - "Configure if changes crafted in the interactive editor are applied directly to the document or are previewed first.", - "Changes are applied directly to the document and are highlighted visually via inline or side-by-side diffs. Ending a session will keep the changes.", + "Overview ruler marker color for inline chat removed content.", + "Configure if changes crafted with inline chat are applied directly to the document or are previewed first.", + "Changes are applied directly to the document, can be highlighted via inline diffs, and accepted/discarded by hunks. Ending a session will keep the changes.", "Changes are previewed only and need to be accepted via the apply button. Ending a session will discard the changes.", - "Changes are applied directly to the document but can be highlighted via inline diffs. Ending a session will keep the changes.", - "Enable/disable showing the diff when edits are generated. Works only with inlineChat.mode equal to live or livePreview." + "Whether to finish an inline chat session when typing outside of changed regions.", + "Whether pending inline chat sessions prevent saving.", + "Whether holding the inline chat keybinding will automatically enable speech recognition.", + "Whether the inline chat also renders an accessible diff viewer for its changes.", + "The accessible diff viewer is based screen reader mode being enabled.", + "The accessible diff viewer is always enabled.", + "The accessible diff viewer is never enabled." + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl": [ + "Waiting for Inline Chat changes to be Accepted or Discarded...", + "Waiting for Inline Chat changes in {0} editors to be Accepted or Discarded..." ], - "vs/workbench/contrib/inlineChat/browser/inlineChatActions": [ - "Start Code Chat", - "Resume Last Dismissed Code Chat", - "Inline Chat", - "Make Request", - "Regenerate Response", - "Regenerate", - "Stop Request", - "Cursor Up", - "Cursor Down", - "Focus Input", - "Previous From History", - "Next From History", - "Discard...", - "Discard", - "Discard to Clipboard", - "Discard to New File", - "Helpful", - "Unhelpful", - "Show Diff", - "&&Show Diff", - "Show Diff", - "&&Show Diff", - "Accept Changes", - "Accept", - "Cancel", - "(Developer) Write Exchange to Clipboard", - "'{0}' and {1} follow ups ({2})", - "View in Chat", - "Show More", - "Show Less" + "vs/workbench/contrib/notebook/browser/notebookEditor": [ + "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", + "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", + "Enable extension for '{0}'", + "Install extension for '{0}'", + "Open As Text", + "The notebook is not displayed in the notebook editor because it is very large ({0}).", + "The notebook is not displayed in the notebook editor because it is very large.", + "Open in Text Editor" ], - "vs/workbench/contrib/files/browser/fileConstants": [ - "Save As...", - "Save", - "Save without Formatting", - "Save All", - "Remove Folder from Workspace", - "New Untitled Text File" + "vs/workbench/contrib/notebook/common/notebookEditorInput": [ + "Notebook '{0}' could not be saved." ], - "vs/workbench/contrib/testing/browser/icons": [ - "View icon of the test view.", - "Icons for test results.", - "Icon of the \"run test\" action.", - "Icon of the \"rerun tests\" action.", - "Icon of the \"run all tests\" action.", - "Icon of the \"debug all tests\" action.", - "Icon of the \"debug test\" action.", - "Icon to cancel ongoing test runs.", - "Icon for the 'Filter' action in the testing view.", - "Icon shown beside hidden tests, when they've been shown.", - "Icon shown when the test explorer is disabled as a tree.", - "Icon shown when the test explorer is disabled as a list.", - "Icon shown to update test profiles.", - "Icon on the button to refresh tests.", - "Icon to turn continuous test runs on.", - "Icon to turn continuous test runs off.", - "Icon when continuous run is on for a test ite,.", - "Icon on the button to cancel refreshing tests.", - "Icon shown for tests that have an error.", + "vs/workbench/contrib/notebook/browser/services/notebookServiceImpl": [ + "Install extension for '{0}'" + ], + "vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor": [ + "Notebook Text Diff" + ], + "vs/workbench/contrib/notebook/browser/services/notebookExecutionServiceImpl": [ + "Executing a notebook cell will run code from this workspace." + ], + "vs/editor/common/languages/modesRegistry": [ + "Plain Text" + ], + "vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl": [ + "Disable other keymaps ({0}) to avoid conflicts between keybindings?", + "Yes", + "No" + ], + "vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl": [ + "Clear Notebook Kernels MRU Cache" + ], + "vs/workbench/contrib/comments/browser/commentReply": [ + "Reply...", + "Type a new comment", + "Reply...", + "Reply..." + ], + "vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl": [ + "Notebook rendering" + ], + "vs/workbench/contrib/notebook/browser/notebookAccessibility": [ + "The notebook view is a collection of code and markdown cells. Code cells can be executed and will produce output directly below the cell.", + "The Edit Cell command ({0}) will focus on the cell input.", + "The Edit Cell command will focus on the cell input and is currently not triggerable by a keybinding.", + "The Quit Edit command ({0}) will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.", + "The Quit Edit command will set focus on the cell container and is currently not triggerable by a keybinding.", + "The Focus Output command ({0}) will set focus in the cell's output.", + "The Quit Edit command will set focus in the cell's output and is currently not triggerable by a keybinding.", + "The Focus Next Cell Editor command ({0}) will set focus in the next cell's editor.", + "The Focus Next Cell Editor command will set focus in the next cell's editor and is currently not triggerable by a keybinding.", + "The Focus Previous Cell Editor command ({0}) will set focus in the previous cell's editor.", + "The Focus Previous Cell Editor command will set focus in the previous cell's editor and is currently not triggerable by a keybinding.", + "The up and down arrows will also move focus between cells while focused on the outer cell container.", + "The Execute Cell command ({0}) executes the cell that currently has focus.", + "The Execute Cell command executes the cell that currently has focus and is currently not triggerable by a keybinding.", + "The Insert Cell Above/Below commands will create new empty code cells", + "The Change Cell to Code/Markdown commands are used to switch between cell types." + ], + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariables": [ + "Notebook Variables" + ], + "vs/workbench/contrib/notebook/browser/controller/coreActions": [ + "Insert Cell", + "Notebook Cell", + "Share", + "Notebook" + ], + "vs/workbench/contrib/notebook/browser/controller/insertCellActions": [ + "Insert Code Cell Above", + "Insert Code Cell Above and Focus Container", + "Insert Code Cell Below", + "Insert Code Cell Below and Focus Container", + "Insert Markdown Cell Above", + "Insert Markdown Cell Below", + "Add Code Cell At Top", + "Add Markdown Cell At Top", + "Code", + "Add Code Cell", + "Add Code", + "Add Code Cell", + "Code", + "Add Code Cell", + "Code", + "Add Code Cell", + "Add Code", + "Add Code Cell", + "Markdown", + "Add Markdown Cell", + "Markdown", + "Add Markdown Cell", + "Markdown", + "Add Markdown Cell" + ], + "vs/workbench/contrib/notebook/browser/controller/sectionActions": [ + "&&Run Cell", + "Run Cell", + "&&Run Cells In Section", + "Run Cells In Section", + "&&Fold Section", + "Fold Section", + "&&Expand Section", + "Expand Section", + "Run Cell", + "Run Cells In Section", + "Fold Section", + "Expand Section" + ], + "vs/workbench/contrib/notebook/browser/controller/executeActions": [ + "Render All Markdown Cells", + "Run All", + "Run All", + "Execute Cell", + "Execute Cell", + "Execute Above Cells", + "Execute Cell and Below", + "Execute Cell and Focus Container", + "Execute Cell and Focus Container", + "Stop Cell Execution", + "Stop Cell Execution", + "Execute Notebook Cell and Select Below", + "Execute Notebook Cell and Insert Below", + "Go To", + "Go to Running Cell", + "Go to Running Cell", + "Go to Running Cell", + "Go to Most Recently Failed Cell", + "Go to Most Recently Failed Cell", + "Go to Most Recently Failed Cell", + "Stop Execution", + "Interrupt" + ], + "vs/workbench/contrib/notebook/browser/controller/layoutActions": [ + "Notebook Line Numbers", + "Settings file to save in", + "User Settings", + "Workspace Settings", + "&&Toggle Notebook Sticky Scroll", + "Toggle Notebook Sticky Scroll", + "&&Toggle Notebook Sticky Scroll", + "Select between Notebook Layouts", + "Customize Notebook Layout", + "Customize Notebook Layout", + "Customize Notebook...", + "Toggle Notebook Line Numbers", + "Toggle Cell Toolbar Position", + "Toggle Breadcrumbs", + "Save Mimetype Display Order", + "Reset Notebook Webview", + "Toggle Notebook Sticky Scroll" + ], + "vs/workbench/contrib/notebook/browser/controller/editActions": [ + "Edit Cell", + "Stop Editing Cell", + "Delete Cell", + "Delete", + "This cell is running, are you sure you want to delete it?", + "Do not ask me again", + "Clear Cell Outputs", + "Clear All Outputs", + "Change Cell Language", + "Change Cell Language", + "({0}) - Current Language", + "({0})", + "Auto Detect", + "languages (identifier)", + "Select Language Mode", + "Unable to detect cell language", + "No notebook editor active at this time", + "The active notebook editor is read-only.", + "convert file", + "change view", + "Select Action", + "Accept Detected Language for Cell", + "Select Indentation" + ], + "vs/workbench/contrib/notebook/browser/controller/cellOutputActions": [ + "Copy Cell Output" + ], + "vs/workbench/contrib/notebook/browser/controller/foldingController": [ + "Fold Cell", + "Unfold Cell", + "Fold Cell" + ], + "vs/workbench/contrib/notebook/browser/contrib/find/notebookFind": [ + "Hide Find in Notebook", + "Find in Notebook" + ], + "vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard": [ + "Copy Cell", + "Cut Cell", + "Paste Cell", + "Paste Cell Above", + "Select All", + "Toggle Notebook Clipboard Troubleshooting" + ], + "vs/workbench/contrib/notebook/browser/contrib/format/formatting": [ + "Format Notebook", + "Format Cell", + "Format Cells", + "Format Notebook" + ], + "vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants": [ + "Formatting", + "Format Notebook", + "Notebook Trim Trailing Whitespace", + "Trim Final New Lines", + "Insert Final New Line", + "Running 'Notebook' code actions", + "Running 'Cell' code actions", + "Getting code actions from '{0}' ([configure]({1})).", + "Applying code action '{0}'." + ], + "vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted": [ + "Reset notebook getting started" + ], + "vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions": [ + "Toggle Cell Toolbar Position" + ], + "vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline": [ + "When enabled, notebook outline will show only markdown cells containing a header.", + "When enabled, notebook outline shows code cells.", + "When enabled, notebook outline shows code cell symbols. Relies on `notebook.outline.showCodeCells` being enabled.", + "When enabled, notebook breadcrumbs contain code cells.", + "When enabled, the Go to Symbol Quick Pick will display full code symbols from the notebook, as well as Markdown headers.", + "Filter Entries", + "Markdown Headers Only", + "Code Cells", + "Code Cell Symbols" + ], + "vs/workbench/contrib/notebook/browser/contrib/navigation/arrow": [ + "Keypresses that should be handled by the focused element in the cell output.", + "Focus Next Cell Editor", + "Focus Previous Cell Editor", + "Focus First Cell", + "Focus Last Cell", + "Focus In Active Cell Output", + "Focus Out Active Cell Output", + "Center Active Cell", + "Cell Cursor Page Up", + "Cell Cursor Page Up Select", + "Cell Cursor Page Down", + "Cell Cursor Page Down Select", + "When enabled cursor can navigate to the next/previous cell when the current cursor in the cell editor is at the first/last line." + ], + "vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile": [ + "Set Profile" + ], + "vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders": [ + "Unknown cell language. Click to search for '{0}' extensions", + "Select Cell Language Mode", + "Accept Detected Language: {0}" + ], + "vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController": [ + "Success", + "Failed", + "Pending", + "Executing", + "Use the links above to file an issue using the issue reporter.", + "**Last Execution** {0}\n\n**Execution Time** {1}\n\n**Overhead Time** {2}\n\n**Render Times**\n\n{3}", + "Quick Actions {0}" + ], + "vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar": [ + "Notebook Kernel Info", + "{0} (suggestion)", + "Notebook Kernel Selection", + "Select Kernel", + "Select Kernel", + "Notebook Editor Selections", + "Cell {0} ({1} selected)", + "Cell {0} of {1}", + "Notebook Indentation", + "Select Indentation" + ], + "vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout": [ + "Toggle Layout Troubleshoot", + "Inspect Notebook Layout", + "Clear Notebook Editor Type Cache" + ], + "vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands": [ + "Toggle Outputs", + "No code actions available", + "Move Cell Up", + "Move Cell Down", + "Copy Cell Up", + "Copy Cell Down", + "Split Cell", + "Join With Previous Cell", + "Join With Next Cell", + "Join Selected Cells", + "Change Cell to Code", + "Change Cell to Markdown", + "Collapse Cell Input", + "Expand Cell Input", + "Collapse Cell Output", + "Expand Cell Output", + "Toggle Outputs", + "Collapse All Cell Inputs", + "Expand All Cell Inputs", + "Collapse All Cell Outputs", + "Expand All Cell Outputs", + "Toggle Scroll Cell Output", + "Show Cell Failure Actions" + ], + "vs/workbench/contrib/notebook/browser/diff/notebookDiffActions": [ + "Revert Metadata", + "Switch Output Rendering", + "Revert Outputs", + "Revert Input", + "Show Previous Change", + "Show Next Change", + "Hide Metadata Differences", + "Hide Outputs Differences", + "Open Text Diff Editor", + "Show Outputs Differences", + "Show Metadata Differences" + ], + "vs/editor/contrib/peekView/browser/peekView": [ + "Whether the current code editor is embedded inside peek", + "Close", + "Background color of the peek view title area.", + "Color of the peek view title.", + "Color of the peek view title info.", + "Color of the peek view borders and arrow.", + "Background color of the peek view result list.", + "Foreground color for line nodes in the peek view result list.", + "Foreground color for file nodes in the peek view result list.", + "Background color of the selected entry in the peek view result list.", + "Foreground color of the selected entry in the peek view result list.", + "Background color of the peek view editor.", + "Background color of the gutter in the peek view editor.", + "Background color of sticky scroll in the peek view editor.", + "Match highlight color in the peek view result list.", + "Match highlight color in the peek view editor.", + "Match highlight border in the peek view editor." + ], + "vs/workbench/contrib/interactive/browser/interactiveEditor": [ + "Type '{0}' code here and press {1} to run" + ], + "vs/workbench/contrib/notebook/browser/notebookIcons": [ + "Configure icon to select a kernel in notebook editors.", + "Icon to execute in notebook editors.", + "Icon to execute above cells in notebook editors.", + "Icon to execute below cells in notebook editors.", + "Icon to stop an execution in notebook editors.", + "Icon to delete a cell in notebook editors.", + "Icon to execute all cells in notebook editors.", + "Icon to edit a cell in notebook editors.", + "Icon to stop editing a cell in notebook editors.", + "Icon to move up a cell in notebook editors.", + "Icon to move down a cell in notebook editors.", + "Icon to clear cell outputs in notebook editors.", + "Icon to split a cell in notebook editors.", + "Icon to indicate a success state in notebook editors.", + "Icon to indicate an error state in notebook editors.", + "Icon to indicate a pending state in notebook editors.", + "Icon to indicate an executing state in notebook editors.", + "Icon to annotate a collapsed section in notebook editors.", + "Icon to annotate an expanded section in notebook editors.", + "Icon to open the notebook in a text editor.", + "Icon to revert in notebook editors.", + "Icon to render output in diff editor.", + "Icon for a mime type in notebook editors.", + "Icon to copy content to clipboard", + "Icon for the previous change action in the diff editor.", + "Icon for the next change action in the diff editor.", + "View icon of the variables view." + ], + "vs/platform/quickinput/browser/helpQuickAccess": [ + "{0}, {1}" + ], + "vs/workbench/contrib/quickaccess/browser/viewQuickAccess": [ + "No matching views", + "Side Bar", + "Panel", + "Secondary Side Bar", + "{0}: {1}", + "Terminal", + "Debug Console", + "Output", + "Open View", + "Quick Open View" + ], + "vs/workbench/contrib/files/browser/fileConstants": [ + "Remove Folder from Workspace", + "Save As...", + "Save", + "Save without Formatting", + "Save All", + "New Untitled Text File" + ], + "vs/workbench/contrib/quickaccess/browser/commandsQuickAccess": [ + "No matching commands", + "Configure Keybinding", + "Ask {0}: {1}", + "{0}: {1}", + "Do you want to clear the history of recently used commands?", + "This action is irreversible!", + "&&Clear", + "Show All Commands", + "Clear Command History" + ], + "vs/workbench/contrib/testing/browser/codeCoverageDecorations": [ + "Toggle Inline Coverage", + "{0} of {1} of branches in {2} were covered.", + "Branch {0} in {1} was not covered.", + "Branch {0} in {1} was executed.", + "Branch {0} in {1} was executed {2} time(s).", + "`{0}` was not executed.", + "`{0}` was executed {1} time(s).", + "`{0}` was executed.", + "Toggle Inline Coverage" + ], + "vs/workbench/contrib/testing/browser/testCoverageBars": [ + "{0}/{1} statements covered ({2})", + "{0}/{1} functions covered ({2})", + "{0}/{1} branches covered ({2})" + ], + "vs/workbench/contrib/testing/browser/icons": [ + "View icon of the test view.", + "Icons for test results.", + "Icon of the \"run test\" action.", + "Icon of the \"rerun tests\" action.", + "Icon of the \"run all tests\" action.", + "Icon of the \"debug all tests\" action.", + "Icon of the \"debug test\" action.", + "Icon of the \"run test with coverage\" action.", + "Icon of the \"run all tests with coverage\" action.", + "Icon to cancel ongoing test runs.", + "Icon for the 'Filter' action in the testing view.", + "Icon shown beside hidden tests, when they've been shown.", + "Icon shown when the test explorer is disabled as a tree.", + "Icon shown when the test explorer is disabled as a list.", + "Icon shown to update test profiles.", + "Icon on the button to refresh tests.", + "Icon to turn continuous test runs on.", + "Icon to turn continuous test runs off.", + "Icon when continuous run is on for a test ite,.", + "Icon on the button to cancel refreshing tests.", + "Icon representing test coverage", + "Icon representing that an element was covered", + "Icon representing a uncovered block without a range", + "Icon shown for tests that have an error.", "Icon shown for tests that failed.", "Icon shown for tests that passed.", "Icon shown for tests that are queued.", "Icon shown for tests that are skipped.", "Icon shown for tests that are in an unset state." ], + "vs/workbench/contrib/testing/browser/testCoverageView": [ + "{0} declarations without coverage...", + "Loading Coverage Details...", + "{0} coverage: {0}%", + "Test Coverage Explorer", + "Sort by Location", + "Files are sorted alphabetically, declarations are sorted by position", + "Sort by Coverage", + "Files and declarations are sorted by total coverage", + "Sort by Name", + "Files and declarations are sorted alphabetically", + "Sort the Test Coverage view...", + "Change Sort Order" + ], "vs/workbench/contrib/testing/browser/testingDecorations": [ "Peek Test Output", "Expected", "Actual", "Click for test options", "Click to debug tests, right click for more options", + "Click to run tests with coverage, right click for more options", "Click to run tests, right click for more options", "Run Test", "Debug Test", + "Run with Coverage", "Execute Using Profile...", "Peek Error", "Reveal in Test Explorer", "Run All Tests", - "Debug All Tests" + "Run All Tests with Coverage", + "Debug All Tests", + "{0} more tests...", + "Select a test to run" ], "vs/workbench/contrib/testing/browser/testingProgressUiService": [ "Running tests...", @@ -24007,6 +26096,7 @@ "Controls the action to take when left-clicking on a test decoration in the gutter.", "Run the test.", "Debug the test.", + "Run the test with coverage.", "Open the context menu for more options.", "Controls whether test decorations are shown in the editor gutter.", "Control whether save all dirty editors before running a test.", @@ -24015,16 +26105,24 @@ "Open the test result view on any test failure", "Open the test explorer when tests start", "Controls when the testing view should open.", - "Always reveal the executed test when `#testing.followRunningTest#` is on. If this setting is turned off, only failed tests will be revealed." + "Always reveal the executed test when `#testing.followRunningTest#` is on. If this setting is turned off, only failed tests will be revealed.", + "Whether test coverage should be down in the File Explorer view.", + "Configures what percentage is displayed by default for test coverage.", + "A calculation of the combined statement, function, and branch coverage.", + "The statement coverage.", + "The minimum of statement, function, and branch coverage.", + "Configures the colors used for percentages in test coverage bars." ], "vs/workbench/contrib/testing/browser/testingOutputPeek": [ "Could not open markdown preview: {0}.\n\nPlease make sure the markdown extension is enabled.", "Test Output", "Expected result", "Actual result", - "Test output is only available for new test runs.", + "The test case did not report any output.", "The test run did not record any output.", - "Close", + "Test output is only available for new test runs.", + "View Test Coverage", + "Close Test Coverage", "Unnamed Task", "+ {0} more lines", "+ 1 more line", @@ -24039,10 +26137,14 @@ "Debug Test", "Go to Source", "Go to Source", + "Close", "Go to Next Test Failure", + "Shows the next failure message in your file", "Go to Previous Test Failure", + "Shows the previous failure message in your file", "Open in Editor", - "Toggle Test History in Peek" + "Toggle Test History in Peek", + "Shows or hides the history of test runs in the peek view" ], "vs/workbench/contrib/testing/common/testingContentProvider": [ "The test run did not record any output." @@ -24065,65 +26167,54 @@ "Indicates whether continous test running is supported", "Indicates whether the parent of a test is continuously running, set in the menu context of test items", "Indicates whether any tests are present in the current editor", + "Indicates whether a test coverage report is open", "Type of the item in the output peek view. Either a \"test\", \"message\", \"task\", or \"result\".", "Controller ID of the current test item", "ID of the current test item, set when creating or opening menus on test items", "Boolean indicating whether the test item has a URI defined", "Boolean indicating whether the test item is hidden", "Value set in `testMessage.contextValue`, available in editor/content and testing/message/context", - "Value available in editor/content and testing/message/context when the result is outdated" + "Value available in editor/content and testing/message/context when the result is outdated", + "Value available testing/item/result indicating the state of the item." ], "vs/workbench/contrib/testing/browser/testingConfigurationUi": [ "Pick a test profile to use", "Update Test Configuration" ], - "vs/workbench/contrib/logs/common/logsActions": [ - "Set Log Level...", - "All", - "Extension Logs", - "Logs", - "Set Log Level", - " {0}: Select log level", - "Select log level", - "Set as Default Log Level", - "Trace", - "Debug", - "Info", - "Warning", - "Error", - "Off", - "Default", - "Open Window Log File (Session)...", - "Current", - "Select Session", - "Select Log file" - ], "vs/workbench/contrib/testing/browser/testExplorerActions": [ + "Turn off Continuous Run", + "Select a profile to update", + "No test continuous run-enabled profiles were found", + "Select profiles to run when files change:", + "Discovering Tests", + "No tests found in this workspace. You may need to install a test provider extension", + "No debuggable tests found in this workspace. You may need to install a test provider extension", + "No tests with coverage runners found in this workspace. You may need to install a test provider extension", + "No tests found here", + "No tests found in the selected file or folder", + "No tests found in this file", + "No coverage information available on the last test run.", + "Run Tests", + "Debug Tests", + "Run Tests with Coverage", "Hide Test", "Unhide Test", "Unhide All Tests", "Debug Test", + "Run Test with Coverage", "Execute Using Profile...", "Run Test", "Select Default Profile", "Turn on Continuous Run", - "Turn off Continuous Run", "Start Continous Run Using...", "Configure Test Profiles", - "Select a profile to update", "Stop Continuous Run", - "No test continuous run-enabled profiles were found", - "Select profiles to run when files change:", "Start Continuous Run", "Get Selected Profiles", "Get Explorer Selection", - "Run Tests", - "Debug Tests", - "Discovering Tests", "Run All Tests", - "No tests found in this workspace. You may need to install a test provider extension", "Debug All Tests", - "No debuggable tests found in this workspace. You may need to install a test provider extension", + "Run All Tests with Coverage", "Cancel Test Run", "View as List", "View as Tree", @@ -24134,132 +26225,62 @@ "Collapse All Tests", "Clear All Results", "Go to Test", - "No tests found here", "Run Test at Cursor", "Debug Test at Cursor", - "No tests found in this file", + "Run Test at Cursor with Coverage", "Run Tests in Current File", "Debug Tests in Current File", + "Run Tests with Coverage in Current File", "Rerun Failed Tests", "Debug Failed Tests", "Rerun Last Run", "Debug Last Run", + "Rerun Last Run with Coverage", "Search for Test Extension", "Peek Output", "Toggle Inline Test Output", "Refresh Tests", - "Cancel Test Refresh" - ], - "vs/editor/contrib/peekView/browser/peekView": [ - "Whether the current code editor is embedded inside peek", - "Close", - "Background color of the peek view title area.", - "Color of the peek view title.", - "Color of the peek view title info.", - "Color of the peek view borders and arrow.", - "Background color of the peek view result list.", - "Foreground color for line nodes in the peek view result list.", - "Foreground color for file nodes in the peek view result list.", - "Background color of the selected entry in the peek view result list.", - "Foreground color of the selected entry in the peek view result list.", - "Background color of the peek view editor.", - "Background color of the gutter in the peek view editor.", - "Background color of sticky scroll in the peek view editor.", - "Match highlight color in the peek view result list.", - "Match highlight color in the peek view editor.", - "Match highlight border in the peek view editor." - ], - "vs/workbench/contrib/notebook/browser/notebookIcons": [ - "Configure icon to select a kernel in notebook editors.", - "Icon to execute in notebook editors.", - "Icon to execute above cells in notebook editors.", - "Icon to execute below cells in notebook editors.", - "Icon to stop an execution in notebook editors.", - "Icon to delete a cell in notebook editors.", - "Icon to execute all cells in notebook editors.", - "Icon to edit a cell in notebook editors.", - "Icon to stop editing a cell in notebook editors.", - "Icon to move up a cell in notebook editors.", - "Icon to move down a cell in notebook editors.", - "Icon to clear cell outputs in notebook editors.", - "Icon to split a cell in notebook editors.", - "Icon to indicate a success state in notebook editors.", - "Icon to indicate an error state in notebook editors.", - "Icon to indicate a pending state in notebook editors.", - "Icon to indicate an executing state in notebook editors.", - "Icon to annotate a collapsed section in notebook editors.", - "Icon to annotate an expanded section in notebook editors.", - "Icon to open the notebook in a text editor.", - "Icon to revert in notebook editors.", - "Icon to render output in diff editor.", - "Icon for a mime type in notebook editors.", - "Icon to copy content to clipboard", - "Icon for the previous change action in the diff editor.", - "Icon for the next change action in the diff editor." - ], - "vs/workbench/contrib/interactive/browser/interactiveEditor": [ - "Type '{0}' code here and press {1} to run" - ], - "vs/platform/quickinput/browser/helpQuickAccess": [ - "{0}, {1}" - ], - "vs/workbench/contrib/quickaccess/browser/commandsQuickAccess": [ - "No matching commands", - "Configure Keybinding", - "Ask {0}: {1}", - "{0}: {1}", - "Show All Commands", - "Clear Command History", - "Do you want to clear the history of recently used commands?", - "This action is irreversible!", - "&&Clear" + "Cancel Test Refresh", + "Clear Coverage", + "Open Coverage" ], - "vs/workbench/contrib/quickaccess/browser/viewQuickAccess": [ - "No matching views", - "Side Bar", - "Panel", - "Secondary Side Bar", - "{0}: {1}", - "Terminal", - "Debug Console", - "Output", - "Open View", - "Quick Open View" + "vs/workbench/contrib/files/browser/views/emptyView": [ + "No Folder Opened" ], - "vs/workbench/contrib/files/browser/fileCommands": [ - "{0} (in file) ↔ {1}", - "Failed to save '{0}': {1}", - "Retry", - "Discard", - "Failed to revert '{0}': {1}", - "Create File" + "vs/workbench/contrib/files/browser/views/explorerView": [ + "Explorer Section: {0}", + "New File...", + "New Folder...", + "Refresh Explorer", + "Forces a refresh of the Explorer.", + "Collapse Folders in Explorer", + "Folds all folders in the Explorer." ], - "vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler": [ - "Use the actions in the editor tool bar to either undo your changes or overwrite the content of the file with your changes.", - "Failed to save '{0}': The content of the file is newer. Please compare your version with the file contents or overwrite the content of the file with your changes.", - "Failed to save '{0}': File is read-only. Select 'Overwrite as Admin' to retry as administrator.", - "Failed to save '{0}': File is read-only. Select 'Overwrite as Sudo' to retry as superuser.", - "Failed to save '{0}': File is read-only. Select 'Overwrite' to attempt to make it writeable.", - "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", - "Failed to save '{0}': Insufficient permissions. Select 'Retry as Sudo' to retry as superuser.", - "Failed to save '{0}': {1}", - "Learn More", - "Don't Show Again", - "Compare", - "{0} (in file) ↔ {1} (in {2}) - Resolve save conflict", - "Overwrite as Admin...", - "Overwrite as Sudo...", - "Retry as Admin...", - "Retry as Sudo...", - "Retry", - "Discard", - "Overwrite", - "Overwrite", - "Configure" + "vs/workbench/contrib/files/browser/views/openEditorsView": [ + "{0} unsaved", + "Open Editors", + "Flip &&Layout", + "Open Editors", + "Toggle Vertical/Horizontal Editor Layout", + "Flip Layout", + "New Untitled Text File" + ], + "vs/workbench/contrib/logs/common/logsActions": [ + "All", + "Extension Logs", + "Logs", + "Set Log Level", + " {0}: Select log level", + "Select log level", + "Set as Default Log Level", + "Default", + "Current", + "Select Session", + "Select Log file", + "Set Log Level...", + "Open Window Log File (Session)..." ], "vs/workbench/contrib/files/browser/fileActions": [ - "New File...", - "New Folder...", "Rename...", "Delete", "Copy", @@ -24275,8 +26296,8 @@ "You are deleting {0} with unsaved changes. Do you want to continue?", "Your changes will be lost if you don't save them.", "This action is irreversible!", - "You can restore these files using the Undo command", - "You can restore this file using the Undo command", + "You can restore these files using the Undo command.", + "You can restore this file using the Undo command.", "You can restore these files from the Recycle Bin.", "You can restore this file from the Recycle Bin.", "You can restore these files from the Trash.", @@ -24302,64 +26323,95 @@ "Are you sure you want to permanently delete '{0}'?", "A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", "&&Replace", - "Compare Active File With...", - "Toggle Auto Save", "Save All in Group", "Close Group", - "Focus on Files Explorer", - "Reveal Active File in Explorer View", - "Open Active File in New Window", "The active editor must contain an openable resource.", "A file or folder name must be provided.", "A file or folder name cannot start with a slash.", "A file or folder **{0}** already exists at this location. Please choose a different name.", "The name **{0}** is not valid as a file or folder name. Please choose a different name.", "Leading or trailing whitespace detected in file or folder name.", - "Compare New Untitled Text Files", - "Compare Active File with Clipboard", "Clipboard ↔ {0}", "Retry", "Create {0}", "Creating {0}", "Rename {0} to {1}", "Renaming {0} to {1}", + "Are you sure you want to paste the following {0} items?", + "Are you sure you want to paste '{0}'?", + "Do not ask me again", + "&&Paste", "File to paste is an ancestor of the destination folder", "Moving {0} files", "Moving {0}", "Move {0} files", "Move {0}", + "The file(s) to paste have been deleted or moved since you copied them. {0}", "Copying {0} files", "Copying {0}", "Paste {0} files", "Paste {0}", - "The file(s) to paste have been deleted or moved since you copied them. {0}", + "New File...", + "New Folder...", + "Compare Active File With...", + "Opens a picker to select a file to diff with the active editor.", + "Toggle Auto Save", + "Toggle the ability to save files automatically after typing", + "Focus on Files Explorer", + "Moves focus to the file explorer view container.", + "Reveal Active File in Explorer View", + "Reveals and selects the active file within the explorer view.", + "Open Active File in New Empty Workspace", + "Opens the active file in a new window with no folders open.", + "Compare New Untitled Text Files", + "Opens a new diff editor with two untitled files.", + "Compare Active File with Clipboard", + "Opens a new diff editor to compare the active file with the contents of the clipboard.", "Set Active Editor Read-only in Session", "Set Active Editor Writeable in Session", "Toggle Active Editor Read-only in Session", "Reset Active Editor Read-only in Session" ], - "vs/workbench/contrib/files/browser/views/emptyView": [ - "No Folder Opened" - ], - "vs/workbench/contrib/files/browser/views/explorerView": [ - "Explorer Section: {0}", - "New File...", - "New Folder...", - "Refresh Explorer", - "Collapse Folders in Explorer" + "vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler": [ + "Use the actions in the editor tool bar to either undo your changes or overwrite the content of the file with your changes.", + "Failed to save '{0}': The content of the file is newer. Please compare your version with the file contents or overwrite the content of the file with your changes.", + "Failed to save '{0}': File is read-only. Select 'Overwrite as Admin' to retry as administrator.", + "Failed to save '{0}': File is read-only. Select 'Overwrite as Sudo' to retry as superuser.", + "Failed to save '{0}': File is read-only. Select 'Overwrite' to attempt to make it writeable.", + "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", + "Failed to save '{0}': Insufficient permissions. Select 'Retry as Sudo' to retry as superuser.", + "Failed to save '{0}': {1}", + "Learn More", + "Don't Show Again", + "Compare", + "{0} (in file) ↔ {1} (in {2}) - Resolve save conflict", + "Overwrite as Admin...", + "Overwrite as Sudo...", + "Retry as Admin...", + "Retry as Sudo...", + "Retry", + "Discard", + "Overwrite", + "Overwrite", + "Configure" ], - "vs/workbench/contrib/files/browser/views/openEditorsView": [ - "Open Editors", - "{0} unsaved", - "Open Editors", - "Toggle Vertical/Horizontal Editor Layout", - "Flip Layout", - "Flip &&Layout", - "New Untitled Text File" + "vs/workbench/contrib/files/browser/fileCommands": [ + "{0} (in file) ↔ {1}", + "Failed to save '{0}': {1}", + "Retry", + "Discard", + "Failed to revert '{0}': {1}", + "Create File" ], "vs/workbench/contrib/files/browser/editors/binaryFileEditor": [ "Binary File Viewer" ], + "vs/workbench/contrib/files/browser/workspaceWatcher": [ + "Unable to watch for file changes. Please follow the instructions link to resolve this issue.", + "Instructions", + "File changes watcher stopped unexpectedly. A reload of the window may enable the watcher again unless the workspace cannot be watched for file changes.", + "Reload" + ], "vs/editor/common/config/editorConfigurationSchema": [ "Editor", "The number of spaces a tab is equal to. This setting is overridden based on the file contents when {0} is on.", @@ -24368,11 +26420,11 @@ "Controls whether {0} and {1} will be automatically detected when a file is opened based on the file contents.", "Remove trailing auto inserted whitespace.", "Special handling for large files to disable certain memory intensive features.", - "Controls whether completions should be computed based on words in the document.", + "Turn off Word Based Suggestions.", "Only suggest words from the active document.", "Suggest words from all open documents of the same language.", "Suggest words from all open documents.", - "Controls from which documents word based completions are computed.", + "Controls whether completions should be computed based on words in the document and from which documents they are computed.", "Semantic highlighting enabled for all color themes.", "Semantic highlighting disabled for all color themes.", "Semantic highlighting is configured by the current color theme's `semanticHighlighting` setting.", @@ -24394,6 +26446,7 @@ "If the diff editor width is smaller than this value, the inline view is used.", "If enabled and the editor width is too small, the inline view is used.", "When enabled, the diff editor shows arrows in its glyph margin to revert changes.", + "When enabled, the diff editor shows a special gutter for revert and stage actions.", "When enabled, the diff editor ignores changes in leading or trailing whitespace.", "Controls whether the diff editor shows +/- indicators for added/removed changes.", "Controls whether the editor shows CodeLens.", @@ -24413,12 +26466,6 @@ "1 unsaved file", "{0} unsaved files" ], - "vs/workbench/contrib/files/browser/workspaceWatcher": [ - "Unable to watch for file changes in this large workspace folder. Please follow the instructions link to resolve this issue.", - "Instructions", - "File changes watcher stopped unexpectedly. A reload of the window may enable the watcher again unless the workspace cannot be watched for file changes.", - "Reload" - ], "vs/workbench/contrib/files/browser/editors/textFileEditor": [ "Text File Editor", "Open Folder", @@ -24434,21 +26481,39 @@ "Discard", "Invoke a code action, like rename, to see a preview of its changes here.", "Cannot apply refactoring because '{0}' has changed in the meantime.", - "Cannot apply refactoring because {0} other files have changed in the meantime.", - "{0} (delete, refactor preview)", - "rename", - "create", - "{0} ({1}, refactor preview)", - "{0} (refactor preview)" + "Cannot apply refactoring because {0} other files have changed in the meantime." ], - "vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview": [ - "Other" + "vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess": [ + "Open a text editor first to go to a line.", + "Go to line {0} and character {1}.", + "Go to line {0}.", + "Current Line: {0}, Character: {1}. Type a line number between 1 and {2} to navigate to.", + "Current Line: {0}, Character: {1}. Type a line number to navigate to." ], - "vs/workbench/contrib/search/browser/searchActionsBase": [ - "Search" + "vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess": [ + "No matching entries", + "Go to &&Symbol in Editor...", + "Type the name of a symbol to go to.", + "Go to Symbol in Editor", + "Go to Symbol in Editor by Category", + "Go to Symbol in Editor..." + ], + "vs/workbench/contrib/search/browser/anythingQuickAccess": [ + "No matching results", + "recently opened", + "recently opened", + "file and symbol results", + "file results", + "{0}, {1}", + "Open Quick Chat", + "Open to the Side", + "Open to the Bottom", + "Remove from Recently Opened", + "{0} unsaved changes" ], "vs/workbench/contrib/search/browser/searchIcons": [ "Icon to make search details visible.", + "Icon to view more context in the search view.", "Icon for toggle the context in the search editor.", "Icon to collapse the replace section in the search view.", "Icon to expand the replace section in the search view.", @@ -24464,60 +26529,14 @@ "Icon for stop in the search view.", "View icon of the search view.", "Icon for the action to open a new search editor.", - "Icon for the action to go to the file of the current search result." - ], - "vs/workbench/contrib/searchEditor/browser/searchEditor": [ - "Toggle Search Details", - "files to include", - "Search Include Patterns", - "files to exclude", - "Search Exclude Patterns", - "Run Search", - "Matched {0} at {1} in file {2}", - "Search", - "Search editor text input box border." - ], - "vs/workbench/contrib/searchEditor/browser/searchEditorInput": [ - "Search: {0}", - "Search: {0}", - "Search" - ], - "vs/workbench/contrib/search/browser/patternInputWidget": [ - "input", - "Search only in Open Editors", - "Use Exclude Settings and Ignore Files" - ], - "vs/workbench/contrib/search/browser/searchMessage": [ - "Unable to open command link from untrusted source: {0}", - "Unable to open unknown link: {0}" - ], - "vs/workbench/browser/parts/views/viewPane": [ - "Icon for an expanded view pane container.", - "Icon for a collapsed view pane container.", - "{0} actions" - ], - "vs/workbench/contrib/search/browser/searchResultsView": [ - "Other files", - "Other files", - "{0} files found", - "{0} file found", - "{0} matches found", - "{0} match found", - "From line {0}", - "{0} more lines", - "Search", - "{0} matches in folder root {1}, Search result", - "{0} matches outside of the workspace, Search result", - "{0} matches in file {1} of folder {2}, Search result", - "'{0}' at column {1} replace {2} with {3}", - "'{0}' at column {1} found {2}" + "Icon for the action to go to the file of the current search result.", + "Icon to show AI results in search.", + "Icon to hide AI results in search." ], - "vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess": [ - "Open a text editor first to go to a line.", - "Go to line {0} and character {1}.", - "Go to line {0}.", - "Current Line: {0}, Character: {1}. Type a line number between 1 and {2} to navigate to.", - "Current Line: {0}, Character: {1}. Type a line number to navigate to." + "vs/workbench/contrib/search/browser/symbolsQuickAccess": [ + "No matching workspace symbols", + "Open to the Side", + "Open to the Bottom" ], "vs/workbench/contrib/search/browser/searchWidget": [ "Replace All (Submit Search to Enable)", @@ -24529,38 +26548,13 @@ "Replace: Type replace term and press Enter to preview", "Replace" ], - "vs/workbench/services/search/common/queryBuilder": [ - "Workspace folder does not exist: {0}" - ], - "vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess": [ - "No matching entries", - "Go to Symbol in Editor...", - "Go to &&Symbol in Editor...", - "Type the name of a symbol to go to.", - "Go to Symbol in Editor", - "Go to Symbol in Editor by Category" - ], - "vs/workbench/contrib/search/browser/symbolsQuickAccess": [ - "No matching workspace symbols", - "Open to the Side", - "Open to the Bottom" - ], - "vs/workbench/contrib/search/browser/anythingQuickAccess": [ - "No matching results", - "recently opened", - "file and symbol results", - "file results", - "{0}, {1}", - "Open Quick Chat", - "Open to the Side", - "Open to the Bottom", - "Remove from Recently Opened", - "{0} unsaved changes" - ], "vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess": [ "See More Files", "Open File", - "More" + "More", + "See in Search Panel", + "Enter a term to search for across your files.", + "No matching results" ], "vs/workbench/contrib/search/browser/searchActionsCopy": [ "Copy", @@ -24568,13 +26562,14 @@ "Copy All" ], "vs/workbench/contrib/search/browser/searchActionsFind": [ + "Expand Recursively", + "Find &&in Files", + "Open a workspace search", + "A set of options for the search", "Restrict Search to Folder", "Exclude Folder from Search", "Reveal in Explorer View", "Find in Files", - "Find &&in Files", - "Open a workspace search", - "A set of options for the search", "Find in Folder...", "Find in Workspace..." ], @@ -24597,114 +26592,197 @@ "Focus Previous Search Result", "Replace in Files" ], - "vs/workbench/contrib/search/browser/searchActionsRemoveReplace": [ - "Dismiss", - "Replace", - "Replace All", - "Replace All" + "vs/workbench/contrib/search/browser/searchActionsRemoveReplace": [ + "Dismiss", + "Replace", + "Replace All", + "Replace All" + ], + "vs/workbench/contrib/search/browser/searchActionsSymbol": [ + "Go to Symbol in Workspace...", + "Go to Symbol in &&Workspace...", + "Go to Symbol in Workspace..." + ], + "vs/workbench/contrib/search/browser/searchActionsTopBar": [ + "Clear Search History", + "Cancel Search", + "Refresh", + "Collapse All", + "Expand All", + "Clear Search Results", + "View as Tree", + "View as List" + ], + "vs/workbench/contrib/search/browser/searchActionsTextQuickAccess": [ + "Quick Search" + ], + "vs/workbench/browser/parts/views/viewPane": [ + "Icon for an expanded view pane container.", + "Icon for a collapsed view pane container.", + "{0} actions", + "Use Alt+F1 for accessibility help {0}" + ], + "vs/workbench/browser/labels": [ + "{0} • Cell {1}", + "{0} • Cell {1}" + ], + "vs/workbench/contrib/search/browser/searchActionsBase": [ + "Search" + ], + "vs/workbench/contrib/search/browser/patternInputWidget": [ + "input", + "Search only in Open Editors", + "Use Exclude Settings and Ignore Files" + ], + "vs/workbench/contrib/search/browser/searchMessage": [ + "Unable to open command link from untrusted source: {0}", + "Unable to open unknown link: {0}" + ], + "vs/workbench/contrib/search/browser/searchResultsView": [ + "Other files", + "Other files", + "{0} files found", + "{0} file found", + "{0} matches found", + "{0} match found", + "From line {0}", + "{0} more lines", + "Search", + "{0} matches in folder root {1}, Search result", + "{0} matches outside of the workspace, Search result", + "{0} matches in file {1} of folder {2}, Search result", + "'{0}' at column {1} replace {2} with {3}", + "'{0}' at column {1} found {2}" + ], + "vs/workbench/services/search/common/queryBuilder": [ + "Workspace folder does not exist: {0}" ], - "vs/workbench/contrib/search/browser/searchActionsSymbol": [ - "Go to Symbol in Workspace...", - "Go to Symbol in Workspace...", - "Go to Symbol in &&Workspace..." + "vs/workbench/contrib/searchEditor/browser/searchEditor": [ + "Toggle Search Details", + "files to include", + "Search Include Patterns", + "files to exclude", + "Search Exclude Patterns", + "Run Search", + "Matched {0} at {1} in file {2}", + "Search", + "Search editor text input box border." ], - "vs/workbench/contrib/search/browser/searchActionsTopBar": [ - "Clear Search History", - "Cancel Search", - "Refresh", - "Collapse All", - "Expand All", - "Clear Search Results", - "View as Tree", - "View as List" + "vs/workbench/contrib/scm/browser/activity": [ + "Source Control", + "{0} pending changes" ], - "vs/workbench/contrib/search/browser/searchActionsTextQuickAccess": [ - "Quick Text Search (Experimental)" + "vs/workbench/contrib/scm/browser/dirtydiffDecorator": [ + "{0} - {1} of {2} changes", + "{0} - {1} of {2} change", + "{0} of {1} changes", + "{0} of {1} change", + "Close", + "Show Previous Change", + "Show Next Change", + "Next &&Change", + "Previous &&Change", + "Go to Previous Change", + "Go to Next Change", + "Editor gutter background color for lines that are modified.", + "Editor gutter background color for lines that are added.", + "Editor gutter background color for lines that are deleted.", + "Minimap gutter background color for lines that are modified.", + "Minimap gutter background color for lines that are added.", + "Minimap gutter background color for lines that are deleted.", + "Overview ruler marker color for modified content.", + "Overview ruler marker color for added content.", + "Overview ruler marker color for deleted content.", + "Added lines", + "Changed lines", + "Removed lines" ], - "vs/workbench/contrib/debug/browser/debugColors": [ - "Debug toolbar background color.", - "Debug toolbar border color.", - "Debug toolbar icon for start debugging.", - "Debug toolbar icon for pause.", - "Debug toolbar icon for stop.", - "Debug toolbar icon for disconnect.", - "Debug toolbar icon for restart.", - "Debug toolbar icon for step over.", - "Debug toolbar icon for step into.", - "Debug toolbar icon for step over.", - "Debug toolbar icon for continue.", - "Debug toolbar icon for step back." + "vs/workbench/contrib/searchEditor/browser/searchEditorInput": [ + "Icon of the search editor label.", + "Search: {0}", + "Search: {0}", + "Search" ], - "vs/workbench/contrib/debug/browser/debugConsoleQuickAccess": [ - "Start a New Debug Session" + "vs/workbench/contrib/scm/browser/scmViewPaneContainer": [ + "Source Control" ], - "vs/workbench/contrib/debug/browser/callStackView": [ - "Running", - "Show More Stack Frames", - "Session", - "Running", - "Restart Frame", - "Load More Stack Frames", - "Show {0} More: {1}", - "Show {0} More Stack Frames", - "Paused on {0}", - "Paused", - "Debug Call Stack", - "Thread {0} {1}", - "Stack Frame {0}, line {1}, {2}", - "Running", - "Session {0} {1}", - "Show {0} More Stack Frames", - "Collapse All" + "vs/workbench/contrib/scm/browser/scmRepositoriesViewPane": [ + "Source Control Repositories" ], - "vs/workbench/contrib/debug/browser/debugCommands": [ - "Debug", - "Restart", - "Step Over", - "Step Into", - "Step Into Target", - "Step Out", - "Pause", - "Disconnect", - "Disconnect and Suspend", - "Stop", - "Continue", - "Focus Session", - "Select and Start Debugging", - "Open '{0}'", - "Start Debugging", - "Start Without Debugging", - "Focus Next Debug Console", - "Focus Previous Debug Console", - "Open Loaded Script...", - "Navigate to Top of Call Stack", - "Navigate to Bottom of Call Stack", - "Navigate Up Call Stack", - "Navigate Down Call Stack", - "Select Debug Console", - "Select Debug Session", - "Choose the specific location", - "No executable code is associated at the current cursor position.", - "Jump to Cursor", - "No step targets available", - "Add Configuration...", - "Add Inline Breakpoint" + "vs/workbench/contrib/workspace/common/workspace": [ + "Whether the workspace trust feature is enabled.", + "Whether the current workspace has been trusted by the user." + ], + "vs/workbench/contrib/scm/browser/scmViewPane": [ + "History item additions foreground color.", + "History item deletions foreground color.", + "History item statistics border color.", + "History item selected statistics border color.", + "{0} file changed", + "{0} files changed", + "{0} insertion{1}", + "{0} insertions{1}", + "{0} deletion{1}", + "{0} deletions{1}", + "Source Control Management", + "Source Control Input", + "View & Sort", + "Incoming & Outgoing", + "Repositories", + "Show Incoming Changes", + "Show Incoming Changes", + "Always", + "Auto", + "Never", + "Show Outgoing Changes", + "Show Outgoing Changes", + "Always", + "Auto", + "Never", + "Show Changes Summary", + "View as List", + "View as Tree", + "Sort by Discovery Time", + "Sort by Name", + "Sort by Path", + "Sort Changes by Name", + "Sort Changes by Path", + "Sort Changes by Status", + "Collapse All Repositories", + "Expand All Repositories", + "More Actions...", + "Cancel", + "Close", + "Incoming/Outgoing", + "Incoming and outgoing changes", + "Incoming", + "Incoming changes", + "Outgoing", + "Outgoing changes", + "Incoming changes from {0}", + "Outgoing changes to {0}", + "All Changes" ], "vs/workbench/contrib/debug/browser/breakpointsView": [ "Unverified Exception Breakpoint", "Expression condition: {0}", - "Expression: {0} | Hit Count: {1}", + "Condition: {0} | Hit Count: {1}", "Function breakpoints are not supported by this debug type", "Data breakpoints are not supported by this debug type", "Read", "Write", "Access", + "Condition: {0} | Hit Count: {1}", "Function to break on", "Type function breakpoint.", "Break when expression evaluates to true", "Type expression. Function breakpoint will break when expression evaluates to true", "Break when hit count is met", "Type hit count. Function breakpoint will break when hit count is met.", + "Break when expression evaluates to true", + "Type expression. Data breakpoint will break when expression evaluates to true", + "Break when hit count is met", + "Type hit count. Data breakpoint will break when hit count is met.", "Type exception breakpoint condition", "Break when expression evaluates to true", "Breakpoints", @@ -24716,7 +26794,7 @@ "Data Breakpoint", "Function breakpoints not supported by this debug type", "Function Breakpoint", - "Expression condition: {0}", + "Condition: {0}", "Hit Count: {0}", "Instruction breakpoints not supported by this debug type", "Instruction breakpoint at address {0}", @@ -24724,25 +26802,99 @@ "Hit Count: {0}", "Breakpoints of this type are not supported by the debugger", "Log Message: {0}", - "Expression condition: {0}", + "Condition: {0}", "Hit Count: {0}", + "Hit after breakpoint: {0}", "Breakpoint", - "Add Function Breakpoint", "&&Function Breakpoint...", - "Toggle Activate Breakpoints", + "Failed to set data breakpoint at {0}: {1}", + "Select the access type to monitor", + "Enter a memory range in which to break", + "Absolute range (0x1234 - 0x1300) or range of bytes after an address (0x1234 + 0xff)", + "Address should be a range of numbers the form \"[Start] - [End]\" or \"[Start] + [Bytes]\"", + "Number must be a decimal integer or hex value starting with \"0x\", got {0}", + "&&Data Breakpoint...", "Remove Breakpoint", - "Remove All Breakpoints", "Remove &&All Breakpoints", - "Enable All Breakpoints", "&&Enable All Breakpoints", - "Disable All Breakpoints", "Disable A&&ll Breakpoints", - "Reapply All Breakpoints", "Edit Condition...", "Edit Condition...", "Edit Hit Count...", - "Edit Function Breakpoint...", - "Edit Hit Count..." + "Edit Function Condition...", + "Edit Hit Count...", + "Edit Mode...", + "Select Breakpoint Mode", + "Add Function Breakpoint", + "Add Data Breakpoint at Address", + "Edit Address...", + "Toggle Activate Breakpoints", + "Remove All Breakpoints", + "Enable All Breakpoints", + "Disable All Breakpoints", + "Reapply All Breakpoints" + ], + "vs/workbench/contrib/debug/browser/callStackView": [ + "Running", + "Show More Stack Frames", + "Session", + "Running", + "Restart Frame", + "Load More Stack Frames", + "Show {0} More: {1}", + "Show {0} More Stack Frames", + "Paused on {0}", + "Paused", + "Debug Call Stack", + "Thread {0} {1}", + "Stack Frame {0}, line {1}, {2}", + "Running", + "Session {0} {1}", + "Show {0} More Stack Frames", + "Collapse All" + ], + "vs/workbench/contrib/debug/browser/debugColors": [ + "Debug toolbar background color.", + "Debug toolbar border color.", + "Debug toolbar icon for start debugging.", + "Debug toolbar icon for pause.", + "Debug toolbar icon for stop.", + "Debug toolbar icon for disconnect.", + "Debug toolbar icon for restart.", + "Debug toolbar icon for step over.", + "Debug toolbar icon for step into.", + "Debug toolbar icon for step over.", + "Debug toolbar icon for continue.", + "Debug toolbar icon for step back." + ], + "vs/workbench/contrib/debug/browser/debugConsoleQuickAccess": [ + "Start a New Debug Session" + ], + "vs/workbench/contrib/debug/browser/debugEditorActions": [ + "Toggle &&Breakpoint", + "Debug: Add Conditional Breakpoint...", + "&&Conditional Breakpoint...", + "Debug: Add Logpoint...", + "&&Logpoint...", + "Debug: Add Triggered Breakpoint...", + "&&Triggered Breakpoint...", + "Debug: Edit Breakpoint", + "&&Edit Breakpoint", + "&&DisassemblyView", + "&&ToggleSource", + "Debug: Show Hover", + "Step targets are not available here", + "Step Into Target", + "Debug: Go to Next Breakpoint", + "Debug: Go to Previous Breakpoint", + "Close Exception Widget", + "Debug: Toggle Breakpoint", + "Open Disassembly View", + "Toggle Source Code in Disassembly View", + "Shows or hides source code in disassembly", + "Run to Cursor", + "Evaluate in Debug Console", + "Add to Watch" ], "vs/workbench/contrib/debug/browser/debugIcons": [ "View icon of the debug console view.", @@ -24755,6 +26907,7 @@ "Icon for breakpoints.", "Icon for disabled breakpoints.", "Icon for unverified breakpoints.", + "Icon for breakpoints waiting on another breakpoint.", "Icon for function breakpoints.", "Icon for disabled function breakpoints.", "Icon for unverified function breakpoints.", @@ -24795,34 +26948,22 @@ "Icon for the Remove action in the watch view.", "Icon for the add action in the watch view.", "Icon for the add function breakpoint action in the watch view.", + "Icon for the add data breakpoint action in the breakpoints view.", "Icon for the Remove All action in the breakpoints view.", - "Icon for the activate action in the breakpoints view.", - "Icon for the debug evaluation input marker.", - "Icon for the debug evaluation prompt.", - "Icon for the inspect memory action." - ], - "vs/workbench/contrib/debug/browser/debugEditorActions": [ - "Debug: Toggle Breakpoint", - "Toggle &&Breakpoint", - "Debug: Add Conditional Breakpoint...", - "&&Conditional Breakpoint...", - "Debug: Add Logpoint...", - "&&Logpoint...", - "Debug: Edit Breakpoint", - "&&Edit Breakpoint", - "Open Disassembly View", - "&&DisassemblyView", - "Toggle Source Code in Disassembly View", - "&&ToggleSource", - "Run to Cursor", - "Evaluate in Debug Console", - "Add to Watch", - "Debug: Show Hover", - "Step targets are not available here", - "Step Into Target", - "Debug: Go to Next Breakpoint", - "Debug: Go to Previous Breakpoint", - "Close Exception Widget" + "Icon for the activate action in the breakpoints view.", + "Icon for the debug evaluation input marker.", + "Icon for the debug evaluation prompt.", + "Icon for the inspect memory action." + ], + "vs/workbench/contrib/debug/browser/debugQuickAccess": [ + "No matching launch configurations", + "Configure Launch Configuration", + "contributed", + "Remove Launch Configuration", + "{0} contributed configurations", + "configure", + "Add Config ({0})...", + "Add Configuration..." ], "vs/workbench/contrib/debug/browser/debugService": [ "1 active session", @@ -24847,26 +26988,52 @@ "Added breakpoint, line {0}, file {1}", "Removed breakpoint, line {0}, file {1}" ], - "vs/workbench/contrib/debug/browser/debugQuickAccess": [ - "No matching launch configurations", - "Configure Launch Configuration", - "contributed", - "Remove Launch Configuration", - "{0} contributed configurations", - "configure", - "Add Config ({0})...", + "vs/workbench/contrib/debug/browser/debugCommands": [ + "Open '{0}'", + "Choose the specific location", + "No executable code is associated at the current cursor position.", + "Jump to Cursor", + "No step targets available", + "Add Inline Breakpoint", + "Debug", + "Restart", + "Step Over", + "Step Into", + "Step Into Target", + "Step Out", + "Pause", + "Disconnect", + "Disconnect and Suspend", + "Stop", + "Continue", + "Focus Session", + "Select and Start Debugging", + "Start Debugging", + "Start Without Debugging", + "Focus Next Debug Console", + "Focus Previous Debug Console", + "Open Loaded Script...", + "Navigate to Top of Call Stack", + "Navigate to Bottom of Call Stack", + "Navigate Up Call Stack", + "Navigate Down Call Stack", + "Copy as Expression", + "Copy Value", + "Add to Watch", + "Select Debug Console", + "Select Debug Session", "Add Configuration..." ], - "vs/workbench/contrib/debug/browser/debugToolBar": [ - "More...", - "Step Back", - "Reverse" - ], "vs/workbench/contrib/debug/browser/debugStatus": [ "Debug", "Debug: {0}", "Select and start debug configuration" ], + "vs/workbench/contrib/debug/browser/debugToolBar": [ + "More...", + "Step Back", + "Reverse" + ], "vs/workbench/contrib/debug/browser/disassemblyView": [ "Disassembly not available.", "instructions", @@ -24876,14 +27043,6 @@ "Bytes", "Instruction" ], - "vs/workbench/contrib/debug/browser/loadedScriptsView": [ - "Debug Session", - "Debug Loaded Scripts", - "Workspace folder {0}, loaded script, debug", - "Session {0}, loaded script, debug", - "Folder {0}, loaded script, debug", - "{0}, loaded script, debug" - ], "vs/workbench/contrib/debug/browser/statusbarColorProvider": [ "Status bar background color when a program is being debugged. The status bar is shown in the bottom of the window", "Status bar foreground color when a program is being debugged. The status bar is shown in the bottom of the window", @@ -24892,15 +27051,33 @@ ], "vs/workbench/contrib/debug/browser/variablesView": [ "Type new variable value", + "Remove Visualizer", + "Type new variable value", + "Visualize Variable...", "Debug Variables", "Scope {0}", "{0}, value {1}", - "Inspecting binary data requires the Hex Editor extension. Would you like to install it now?", - "Cancel", - "Install", - "Installing the Hex Editor...", + "Inspecting binary data requires this extension.", + "Collapse All" + ], + "vs/workbench/contrib/debug/browser/loadedScriptsView": [ + "Debug Session", + "Debug Loaded Scripts", + "Workspace folder {0}, loaded script, debug", + "Session {0}, loaded script, debug", + "Folder {0}, loaded script, debug", + "{0}, loaded script, debug", "Collapse All" ], + "vs/workbench/contrib/debug/browser/welcomeView": [ + "[Open a file](command:{0}) which can be debugged or run.", + "Run and Debug", + "Show all automatic debug configurations", + "To customize Run and Debug [create a launch.json file](command:{0}).", + "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", + "All debug extensions are disabled. Enable a debug extension or install a new one from the Marketplace.", + "Run" + ], "vs/workbench/contrib/debug/browser/watchExpressionsView": [ "Type new value", "Type watch expression", @@ -24912,98 +27089,28 @@ "Add Expression", "Remove All Expressions" ], - "vs/workbench/contrib/debug/browser/welcomeView": [ - "Run", - "[Open a file](command:{0}) which can be debugged or run.", - "Run and Debug", - "Show all automatic debug configurations", - "To customize Run and Debug [create a launch.json file](command:{0}).", - "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", - "All debug extensions are disabled. Enable a debug extension or install a new one from the Marketplace." - ], "vs/workbench/contrib/debug/common/debugContentProvider": [ "Unable to resolve the resource without a debug session", "Could not load source '{0}': {1}.", "Could not load source '{0}'." ], - "vs/workbench/contrib/debug/common/disassemblyViewInput": [ - "Disassembly" - ], "vs/workbench/contrib/debug/common/debugLifecycle": [ "There is an active debug session, are you sure you want to stop it?", "There are active debug sessions, are you sure you want to stop them?", "&&Stop Debugging" ], - "vs/workbench/contrib/debug/browser/breakpointWidget": [ - "Message to log when breakpoint is hit. Expressions within {} are interpolated. '{0}' to accept, '{1}' to cancel.", - "Break when hit count condition is met. '{0}' to accept, '{1}' to cancel.", - "Break when expression evaluates to true. '{0}' to accept, '{1}' to cancel.", - "Expression", - "Hit Count", - "Log Message", - "Breakpoint Type" - ], - "vs/workbench/contrib/scm/browser/activity": [ - "Source Control", - "{0} pending changes" - ], - "vs/workbench/contrib/scm/browser/scmViewPaneContainer": [ - "Source Control" - ], - "vs/workbench/contrib/scm/browser/dirtydiffDecorator": [ - "{0} - {1} of {2} changes", - "{0} - {1} of {2} change", - "{0} of {1} changes", - "{0} of {1} change", - "Close", - "Show Previous Change", - "Show Next Change", - "Next &&Change", - "Previous &&Change", - "Go to Previous Change", - "Go to Next Change", - "Editor gutter background color for lines that are modified.", - "Editor gutter background color for lines that are added.", - "Editor gutter background color for lines that are deleted.", - "Minimap gutter background color for lines that are modified.", - "Minimap gutter background color for lines that are added.", - "Minimap gutter background color for lines that are deleted.", - "Overview ruler marker color for modified content.", - "Overview ruler marker color for added content.", - "Overview ruler marker color for deleted content." - ], - "vs/workbench/contrib/scm/browser/scmRepositoriesViewPane": [ - "Source Control Repositories" - ], - "vs/workbench/contrib/workspace/common/workspace": [ - "Whether the workspace trust feature is enabled.", - "Whether the current workspace has been trusted by the user." + "vs/workbench/contrib/debug/common/disassemblyViewInput": [ + "Icon of the disassembly editor label.", + "Disassembly" ], - "vs/workbench/contrib/scm/browser/scmViewPane": [ - "Source Control Management", - "Source Control Input", - "View & Sort", - "Repositories", - "View as List", - "View as Tree", - "Sort by Discovery Time", - "Sort by Name", - "Sort by Path", - "Sort Changes by Name", - "Sort Changes by Path", - "Sort Changes by Status", - "Collapse All Repositories", - "Expand All Repositories", - "Close", - "SCM Provider separator border." + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariableCommands": [ + "Copy Value", + "Execute Notebook Variable Provider" ], - "vs/workbench/contrib/scm/browser/scmSyncViewPane": [ - "Source Control Sync", - "Incoming Changes", - "Outgoing Changes", - "Refresh", - "View as List", - "View as Tree" + "vs/workbench/contrib/debug/browser/debugHover": [ + "Hold {0} key to switch to editor language hover", + "Debug Hover", + "{0}, value {1}, variables, debug" ], "vs/workbench/contrib/debug/browser/exceptionWidget": [ "Exception widget border color.", @@ -25012,11 +27119,6 @@ "Exception has occurred.", "Close" ], - "vs/workbench/contrib/debug/browser/debugHover": [ - "Hold {0} key to switch to editor language hover", - "Debug Hover", - "{0}, value {1}, variables, debug" - ], "vs/workbench/contrib/debug/common/debugModel": [ "Invalid variable attributes", "Please start a debug session to evaluate expressions", @@ -25026,38 +27128,21 @@ "Running", "Unverified breakpoint. File is modified, please restart debug session." ], - "vs/platform/history/browser/contextScopedHistoryWidget": [ - "Whether suggestion are visible" - ], - "vs/workbench/contrib/debug/browser/debugActionViewItems": [ - "Debug Launch Configurations", - "No Configurations", - "Add Config ({0})...", - "Add Configuration...", - "Debug Session" - ], - "vs/platform/actions/browser/menuEntryActionViewItem": [ - "{0} ({1})", - "{0} ({1})", - "{0}\n[{1}] {2}" - ], - "vs/workbench/contrib/debug/browser/linkDetector": [ - "follow link using forwarded port", - "follow link", - "Cmd + click to {0}\n{1}", - "Ctrl + click to {0}\n{1}", - "Cmd + click to {0}", - "Ctrl + click to {0}" - ], - "vs/workbench/contrib/debug/common/replModel": [ - "Console was cleared" - ], - "vs/workbench/contrib/debug/browser/replViewer": [ - "Debug Console", - "Variable {0}, value {1}", - ", occurred {0} times", - "Debug console variable {0}, value {1}", - "Debug console group {0}" + "vs/workbench/contrib/debug/browser/breakpointWidget": [ + "Message to log when breakpoint is hit. Expressions within {} are interpolated. '{0}' to accept, '{1}' to cancel.", + "Break when hit count condition is met. '{0}' to accept, '{1}' to cancel.", + "Break when expression evaluates to true. '{0}' to accept, '{1}' to cancel.", + "Expression", + "Hit Count", + "Log Message", + "Wait for Breakpoint", + "Breakpoint Type", + "Mode", + "None", + "Loading...", + "Could not load source.", + "Select breakpoint", + "Ok" ], "vs/workbench/contrib/markers/browser/markersView": [ "Showing {0} of {1}", @@ -25067,7 +27152,6 @@ ], "vs/workbench/contrib/markers/browser/messages": [ "Toggle Problems (Errors, Warnings, Infos)", - "Focus Problems (Errors, Warnings, Infos)", "Problems View", "Controls whether Problems view should automatically reveal files when opening them.", "Controls the default view mode of the Problems view.", @@ -25075,7 +27159,6 @@ "Controls the order in which problems are navigated.", "Navigate problems ordered by severity", "Navigate problems ordered by position", - "Problems", "No problems have been detected in the workspace.", "No problems have been detected in the current file.", "No results found with provided filter criteria.", @@ -25103,95 +27186,215 @@ "[Ln {0}, Col {1}]", "{0} problems in file {1} of folder {2}", " This problem has references to {0} locations.", - "Error: {0} at line {1} and character {2}.{3} generated by {4}", - "Error: {0} at line {1} and character {2}.{3}", - "Warning: {0} at line {1} and character {2}.{3} generated by {4}", - "Warning: {0} at line {1} and character {2}.{3}", - "Info: {0} at line {1} and character {2}.{3} generated by {4}", - "Info: {0} at line {1} and character {2}.{3}", - "Problem: {0} at line {1} and character {2}.{3} generated by {4}", - "Problem: {0} at line {1} and character {2}.{3}", - "{0} at line {1} and character {2} in {3}", - "Show Errors and Warnings" - ], - "vs/workbench/browser/parts/views/viewFilter": [ - "More Filters..." - ], - "vs/workbench/contrib/mergeEditor/browser/commands/commands": [ - "Open Merge Editor", - "Mixed Layout", - "Column Layout", - "Show Non-Conflicting Changes", - "Show Base", - "Show Base Top", - "Show Base Center", - "Merge Editor", - "Open File", - "Go to Next Unhandled Conflict", - "Go to Previous Unhandled Conflict", - "Toggle Current Conflict from Left", - "Toggle Current Conflict from Right", - "Compare Input 1 With Base", - "Compare With Base", - "Compare Input 2 With Base", - "Compare With Base", - "Open Base File", - "Accept All Changes from Left", - "Accept All Changes from Right", - "Reset Result", - "Reset", - "Reset Choice for 'Close with Conflicts'", - "Complete Merge", - "Do you want to complete the merge of {0}?", - "The file contains unhandled conflicts.", - "&&Complete with Conflicts" + "Error: {0} at line {1} and character {2}.{3} generated by {4}", + "Error: {0} at line {1} and character {2}.{3}", + "Warning: {0} at line {1} and character {2}.{3} generated by {4}", + "Warning: {0} at line {1} and character {2}.{3}", + "Info: {0} at line {1} and character {2}.{3} generated by {4}", + "Info: {0} at line {1} and character {2}.{3}", + "Problem: {0} at line {1} and character {2}.{3} generated by {4}", + "Problem: {0} at line {1} and character {2}.{3}", + "{0} at line {1} and character {2} in {3}", + "Show Errors and Warnings", + "Focus Problems (Errors, Warnings, Infos)", + "Problems" ], "vs/workbench/contrib/markers/browser/markersFileDecorations": [ "Problems", "1 problem in this file", "{0} problems in this file", - "Show Errors & Warnings on files and folder." + "Show Errors & Warnings on files and folder. Overwritten by `#problems.visibility#` when it is off." ], - "vs/workbench/contrib/mergeEditor/browser/mergeEditorInput": [ - "Merging: {0}" + "vs/workbench/browser/parts/views/viewFilter": [ + "More Filters..." ], - "vs/workbench/contrib/mergeEditor/browser/commands/devCommands": [ - "Merge Editor (Dev)", - "Copy Merge Editor State as JSON", - "Merge Editor", - "No active merge editor", - "Merge Editor", - "Successfully copied merge editor state", - "Save Merge Editor State to Folder", - "Merge Editor", - "No active merge editor", - "Select folder to save to", - "Merge Editor", - "Successfully saved merge editor state to folder", - "Load Merge Editor State from Folder", - "Select folder to save to" + "vs/platform/actions/browser/menuEntryActionViewItem": [ + "{0} ({1})", + "{0} ({1})", + "{0}\n[{1}] {2}" ], - "vs/workbench/contrib/mergeEditor/browser/view/mergeEditor": [ - "Text Merge Editor" + "vs/workbench/contrib/debug/browser/debugActionViewItems": [ + "Debug Launch Configurations", + "No Configurations", + "Add Config ({0})...", + "Add Configuration...", + "Debug Session" ], - "vs/workbench/contrib/comments/common/commentContextKeys": [ - "Whether the position at the active cursor has a commenting range", - "Whether the active editor has a commenting range", - "Whether the open workspace has either comments or commenting ranges.", - "Set when the comment thread has no comments", - "Set when the comment has no input", - "The context value of the comment", - "The context value of the comment thread", - "The comment controller id associated with a comment thread", - "Set when the comment is focused" + "vs/workbench/contrib/comments/browser/commentsTreeViewer": [ + "{0} replies", + "1 reply", + "1 comment", + "Image: {0}", + "Image", + "Outdated", + "[Ln {0}]", + "[Ln {0}-{1}]", + "Last reply from {0}", + "Comments" ], - "vs/workbench/contrib/url/browser/trustedDomains": [ - "Manage Trusted Domains", - "Trust {0}", - "Trust {0} on all ports", - "Trust {0} and all its subdomains", - "Trust all domains (disables link protection)", - "Manage Trusted Domains" + "vs/workbench/contrib/comments/browser/commentsView": [ + "Filter (e.g. text, author)", + "Filter comments", + "Showing {0} of {1}", + "Inspect this in the accessible view ({0}).\n", + "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.\n", + "Outdated from {0} at line {1} column {2} in {3},{4} comment: {5}", + "{0} at line {1} column {2} in {3},{4} comment: {5}", + "Outdated from {0} in {1},{2} comment: {3}", + "{0} in {1},{2} comment: {3}", + "{0} {1}", + " {0} replies,", + "Comments for current workspace", + "Comments in {0}, full path {1}" + ], + "vs/workbench/contrib/comments/browser/commentsController": [ + "Line {0}", + "Lines {0} to {1}", + "Editor has commenting ranges, run the command Open Accessibility Help ({0}), for more information.", + "Editor has commenting ranges, run the command Open Accessibility Help, which is currently not triggerable via keybinding, for more information.", + "Editor has commenting ranges.", + "Select Comment Provider" + ], + "vs/workbench/contrib/accessibility/browser/accessibilityConfiguration": [ + "This setting is deprecated. Use the `signals` settings instead.", + "Accessibility", + "Enable sound when a screen reader is attached.", + "Enable sound.", + "Disable sound.", + "Enable announcement, will only play when in screen reader optimized mode.", + "Disable announcement.", + "Provide information about how to access the terminal accessibility help menu when the terminal is focused.", + "Provide information about how to navigate changes in the diff editor when it is focused.", + "Provide information about how to access the chat help menu when the chat input is focused.", + "Provide information about how to access the inline editor chat accessibility help menu and alert with hints that describe how to use the feature when the input is focused.", + "Provide information about how to access the inline completions hover and Accessible View.", + "Provide information about how to change a keybinding in the keybindings editor when a row is focused.", + "Provide information about how to focus the cell container or inner editor when a notebook cell is focused.", + "Provide information about how to open the hover in an Accessible View.", + "Provide information about how to open the notification in an Accessible View.", + "Provide information about relevant actions in an empty text editor.", + "Provide information about actions that can be taken in the comment widget or in a file which contains comments.", + "Indicate when a diff editor becomes the active editor.", + "Indicates when a file is saved. Also see {0}.", + "Indicates when a file is saved via user gesture.", + "Indicates whenever is a file is saved, including auto save.", + "Never alerts.", + "Indicates when a feature is cleared (for example, the terminal, Debug Console, or Output channel). Also see {0}.", + "Indicates when a file or notebook cell is formatted. Also see {0}.", + "Indicates when a file is formatted via user gesture.", + "Indicates whenever is a file is formatted, including auto save, on cell execution, and more.", + "Never alerts.", + "Indicates when the debugger breaks. Also see {0}.", + "Indicates when the active line has an error. Also see {0}.", + "Indicates when the active line has a warning. Also see {0}.", + "Indicates when the active line has a folded area that can be unfolded. Also see {0}.", + "Indicates when there is an available terminal quick fix. Also see {0}.", + "Indicates when the terminal bell is activated.", + "Indicates when a terminal command fails (non-zero exit code). Also see {0}.", + "Indicates when a task fails (non-zero exit code). Also see {0}.", + "Indicates when a task completes successfully (zero exit code). Also see {0}.", + "Indicates when a chat request is sent. Also see {0}.", + "Indicates when a chat response is pending. Also see {0}.", + "Indicates when there are no inlay hints. Also see {0}.", + "Indicates when on a line with a breakpoint. Also see {0}.", + "Indicates when a notebook cell completes successfully. Also see {0}.", + "Indicates when a notebook cell fails. Also see {0}.", + "Indicates when the debugger breaks. Also see {0}.", + "On keypress, close the Accessible View and focus the element from which it was invoked.", + "The volume of the sounds in percent (0-100).", + "Whether or not position changes should be debounced", + "Plays a signal when the active line has a breakpoint.", + "Plays a sound when the active line has a breakpoint.", + "Indicates when the active line has a breakpoint.", + "Indicates when the active line has an inline suggestion.", + "Plays a sound when the active line has an inline suggestion.", + "Indicates when the active line has an error.", + "Plays a sound when the active line has an error.", + "Indicates when the active line has an error.", + "Indicates when the active line has a folded area that can be unfolded.", + "Plays a sound when the active line has a folded area that can be unfolded.", + "Indicates when the active line has a folded area that can be unfolded.", + "Plays a signal when the active line has a warning.", + "Plays a sound when the active line has a warning.", + "Indicates when the active line has a warning.", + "Plays a signal when the active line has a warning.", + "Plays a sound when the active line has a warning.", + "Indicates when the active line has a warning.", + "Plays a signal when the active line has a warning.", + "Plays a sound when the active line has a warning.", + "Indicates when the active line has a warning.", + "Plays a signal when the debugger stopped on a breakpoint.", + "Plays a sound when the debugger stopped on a breakpoint.", + "Indicates when the debugger stopped on a breakpoint.", + "Plays a signal when trying to read a line with inlay hints that has no inlay hints.", + "Plays a sound when trying to read a line with inlay hints that has no inlay hints.", + "Indicates when trying to read a line with inlay hints that has no inlay hints.", + "Plays a signal when a task is completed.", + "Plays a sound when a task is completed.", + "Indicates when a task is completed.", + "Plays a signal when a task fails (non-zero exit code).", + "Plays a sound when a task fails (non-zero exit code).", + "Indicates when a task fails (non-zero exit code).", + "Plays a signal when a terminal command fails (non-zero exit code) or when a command with such an exit code is navigated to in the accessible view.", + "Plays a sound when a terminal command fails (non-zero exit code) or when a command with such an exit code is navigated to in the accessible view.", + "Indicates when a terminal command fails (non-zero exit code) or when a command with such an exit code is navigated to in the accessible view.", + "Plays a signal when terminal Quick Fixes are available.", + "Plays a sound when terminal Quick Fixes are available.", + "Indicates when terminal Quick Fixes are available.", + "Plays a signal when the terminal bell is ringing.", + "Plays a sound when the terminal bell is ringing.", + "Indicates when the terminal bell is ringing.", + "Indicates when the focus moves to an inserted line in Accessible Diff Viewer mode or to the next/previous change.", + "Plays a sound when the focus moves to an inserted line in Accessible Diff Viewer mode or to the next/previous change.", + "Indicates when the focus moves to an modified line in Accessible Diff Viewer mode or to the next/previous change.", + "Plays a sound when the focus moves to a modified line in Accessible Diff Viewer mode or to the next/previous change.", + "Indicates when the focus moves to an deleted line in Accessible Diff Viewer mode or to the next/previous change.", + "Plays a sound when the focus moves to an deleted line in Accessible Diff Viewer mode or to the next/previous change.", + "Plays a signal when a notebook cell execution is successfully completed.", + "Plays a sound when a notebook cell execution is successfully completed.", + "Indicates when a notebook cell execution is successfully completed.", + "Plays a signal when a notebook cell execution fails.", + "Plays a sound when a notebook cell execution fails.", + "Indicates when a notebook cell execution fails.", + "Plays a signal when a chat request is made.", + "Plays a sound when a chat request is made.", + "Indicates when a chat request is made.", + "Plays a signal on loop while progress is occurring.", + "Plays a sound on loop while progress is occurring.", + "Alerts on loop while progress is occurring.", + "Indicates when the response has been received.", + "Plays a sound on loop while the response has been received.", + "Indicates when the voice recording has started.", + "Plays a sound when the voice recording has started.", + "Indicates when the voice recording has stopped.", + "Plays a sound when the voice recording has stopped.", + "Plays a signal when a feature is cleared (for example, the terminal, Debug Console, or Output channel).", + "Plays a sound when a feature is cleared.", + "Indicates when a feature is cleared.", + "Plays a signal when a file is saved.", + "Plays a sound when a file is saved.", + "Plays the audio cue when a user explicitly saves a file.", + "Plays the audio cue whenever a file is saved, including auto save.", + "Never plays the audio cue.", + "Indicates when a file is saved.", + "Announces when a user explicitly saves a file.", + "Announces whenever a file is saved, including auto save.", + "Never plays the audio cue.", + "Plays a signal when a file or notebook is formatted.", + "Plays a sound when a file or notebook is formatted.", + "Plays the audio cue when a user explicitly formats a file.", + "Plays the audio cue whenever a file is formatted, including if it is set to format on save, type, or, paste, or run of a cell.", + "Never plays the audio cue.", + "Indicates when a file or notebook is formatted.", + "Announceswhen a user explicitly formats a file.", + "Announces whenever a file is formatted, including if it is set to format on save, type, or, paste, or run of a cell.", + "Never announces.", + "Whether to dim unfocused editors and terminals, which makes it more clear where typed input will go to. This works with the majority of editors with the notable exceptions of those that utilize iframes like notebooks and extension webview editors.", + "The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will only take effect when {0} is enabled.", + "Controls whether the Accessible View is hidden.", + "The duration in milliseconds that voice speech recognition remains active after you stop speaking. For example in a chat session, the transcribed text is submitted automatically after the timeout is met. Set to `0` to disable this feature.", + "The language that voice speech recognition should recognize. Select `auto` to use the configured display language if possible. Note that not all display languages maybe supported by speech recognition", + "Auto (Use Display Language)" ], "vs/workbench/contrib/comments/browser/commentsEditorContribution": [ "Go to Next Commenting Range", @@ -25203,182 +27406,158 @@ "Expand All Comments", "Expand Unresolved Comments" ], - "vs/workbench/contrib/url/browser/trustedDomainsValidator": [ - "Do you want {0} to open the external website?", - "&&Open", - "&&Copy", - "Configure &&Trusted Domains" - ], - "vs/workbench/contrib/webviewPanel/browser/webviewCommands": [ - "Show find", - "Stop find", - "Find next", - "Find previous", - "Reload Webviews" - ], - "vs/workbench/contrib/webviewPanel/browser/webviewEditor": [ - "The viewType of the currently active webview panel." - ], - "vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService": [ - "Open in new browser window", - "Open in default browser", - "Configure default opener...", - "How would you like to open: {0}" - ], - "vs/workbench/contrib/externalUriOpener/common/configuration": [ - "Configure the opener to use for external URIs (http, https).", - "Map URI pattern to an opener id.\nExample patterns: \n{0}", - "Map URI pattern to an opener id.\nExample patterns: \n{0}", - "Open using VS Code's standard opener." - ], - "vs/workbench/contrib/customEditor/common/customEditor": [ - "The viewType of the currently active custom editor." - ], - "vs/workbench/contrib/extensions/common/extensionsInput": [ - "Extension: {0}" + "vs/workbench/contrib/mergeEditor/browser/commands/devCommands": [ + "Merge Editor", + "No active merge editor", + "Merge Editor", + "Successfully copied merge editor state", + "Merge Editor", + "No active merge editor", + "Select folder to save to", + "Merge Editor", + "Successfully saved merge editor state to folder", + "Select folder to save to", + "Merge Editor (Dev)", + "Copy Merge Editor State as JSON", + "Save Merge Editor State to Folder", + "Load Merge Editor State from Folder" ], - "vs/workbench/contrib/extensions/common/extensionsUtils": [ - "Disable other keymaps ({0}) to avoid conflicts between keybindings?", - "Yes", - "No" + "vs/workbench/contrib/mergeEditor/browser/mergeEditorInput": [ + "Merging: {0}" ], - "vs/workbench/contrib/extensions/browser/extensionEditor": [ - "Extension Version", - "Pre-Release", - "Extension name", - "Preview", - "Preview", - "Built-in", - "Publisher", - "Install count", - "Rating", - "Details", - "Extension details, rendered from the extension's 'README.md' file", - "Feature Contributions", - "Lists contributions to VS Code by this extension", - "Changelog", - "Extension update history, rendered from the extension's 'CHANGELOG.md' file", - "Dependencies", - "Lists extensions this extension depends on", - "Extension Pack", - "Lists extensions those will be installed together with this extension", - "Runtime Status", - "Extension runtime status", - "No README available.", - "Readme", - "Extension Pack ({0})", - "No README available.", - "Readme", - "Categories", - "Marketplace", - "Repository", - "License", - "Extension Resources", - "More Info", - "Published", - "Last released", - "Last updated", - "Identifier", - "No Changelog available.", - "Changelog", - "No Contributions", - "No Contributions", - "No Dependencies", - "Activation Event:", - "Startup", - "Activation Time:", - "Activated By:", - "Not yet activated.", - "Uncaught Errors ({0})", - "Messages ({0})", - "No status available.", - "Settings ({0})", - "ID", - "Description", - "Default", - "Debuggers ({0})", - "Name", - "Type", - "View Containers ({0})", - "ID", - "Title", - "Where", - "Views ({0})", - "ID", - "Name", - "Where", - "Localizations ({0})", - "Language ID", - "Language Name", - "Language Name (Localized)", - "Custom Editors ({0})", - "View Type", - "Priority", - "Filename Pattern", - "Code Actions ({0})", - "Title", - "Kind", - "Description", - "Languages", - "Authentication ({0})", - "Label", - "ID", - "Color Themes ({0})", - "File Icon Themes ({0})", - "Product Icon Themes ({0})", - "Colors ({0})", - "ID", - "Description", - "Dark Default", - "Light Default", - "High Contrast Default", - "JSON Validation ({0})", - "File Match", - "Schema", - "Commands ({0})", - "ID", - "Title", - "Keyboard Shortcuts", - "Menu Contexts", - "Languages ({0})", - "ID", - "Name", - "File Extensions", - "Grammar", - "Snippets", - "Activation Events ({0})", - "Notebooks ({0})", - "ID", - "Name", - "Notebook Renderers ({0})", - "Name", - "Mimetypes", - "Find", - "Find Next", - "Find Previous" + "vs/workbench/contrib/mergeEditor/browser/commands/commands": [ + "Compare With Base", + "Compare With Base", + "Reset", + "Do you want to complete the merge of {0}?", + "The file contains unhandled conflicts.", + "&&Complete with Conflicts", + "Open Merge Editor", + "Mixed Layout", + "Column Layout", + "Show Non-Conflicting Changes", + "Show Base", + "Show Base Top", + "Show Base Center", + "Merge Editor", + "Open File", + "Go to Next Unhandled Conflict", + "Go to Previous Unhandled Conflict", + "Toggle Current Conflict from Left", + "Toggle Current Conflict from Right", + "Compare Input 1 With Base", + "Compare Input 2 With Base", + "Open Base File", + "Accept All Changes from Left", + "Accept All Changes from Right", + "Reset Result", + "Reset Choice for 'Close with Conflicts'", + "Complete Merge" ], - "vs/workbench/contrib/extensions/common/extensionsFileTemplate": [ - "Extensions", - "List of extensions which should be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'.", - "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.", - "List of extensions recommended by VS Code that should not be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'.", - "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'." + "vs/workbench/contrib/mergeEditor/browser/view/mergeEditor": [ + "Text Merge Editor" ], - "vs/workbench/contrib/extensions/browser/extensionsActivationProgress": [ - "Activating Extensions..." + "vs/workbench/contrib/url/browser/trustedDomains": [ + "Trust {0}", + "Trust {0} on all ports", + "Trust {0} and all its subdomains", + "Trust all domains (disables link protection)", + "Manage Trusted Domains", + "Manage Trusted Domains" ], - "vs/workbench/contrib/extensions/browser/extensionsDependencyChecker": [ + "vs/workbench/contrib/url/browser/trustedDomainsValidator": [ + "Do you want {0} to open the external website?", + "&&Open", + "&&Copy", + "Configure &&Trusted Domains" + ], + "vs/workbench/contrib/webviewPanel/browser/webviewCommands": [ + "Show find", + "Stop find", + "Find next", + "Find previous", + "Reload Webviews" + ], + "vs/workbench/contrib/webviewPanel/browser/webviewEditor": [ + "The viewType of the currently active webview panel." + ], + "vs/workbench/contrib/customEditor/common/customEditor": [ + "The viewType of the currently active custom editor." + ], + "vs/workbench/contrib/customEditor/browser/customEditorInput": [ + "Unable to open the editor in this window, it contains modifications that can only be saved in the original window.", + "Open in Original Window", + "Unable to move '{0}': The editor contains changes that can only be saved in its current window." + ], + "vs/workbench/contrib/externalUriOpener/common/configuration": [ + "Configure the opener to use for external URIs (http, https).", + "Map URI pattern to an opener id.\nExample patterns: \n{0}", + "Map URI pattern to an opener id.\nExample patterns: \n{0}", + "Open using VS Code's standard opener." + ], + "vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService": [ + "Open in new browser window", + "Open in default browser", + "Configure default opener...", + "How would you like to open: {0}" + ], + "vs/workbench/contrib/extensions/common/extensionsInput": [ + "Icon of the extensions editor label.", + "Extension: {0}" + ], + "vs/workbench/contrib/extensions/browser/extensionsIcons": [ + "View icon of the extensions view.", + "Icon for the 'Manage' action in the extensions view.", + "Icon for the 'Clear Search Result' action in the extensions view.", + "Icon for the 'Refresh' action in the extensions view.", + "Icon for the 'Filter' action in the extensions view.", + "Icon for the 'Install Local Extension in Remote' action in the extensions view.", + "Icon for the 'Install Workspace Recommended Extensions' action in the extensions view.", + "Icon for the 'Configure Recommended Extensions' action in the extensions view.", + "Icon to indicate that an extension is synced.", + "Icon to indicate that an extension is ignored when syncing.", + "Icon to indicate that an extension is remote in the extensions view and editor.", + "Icon shown along with the install count in the extensions view and editor.", + "Icon shown along with the rating in the extensions view and editor.", + "Icon used for the verified extension publisher in the extensions view and editor.", + "Icon shown for extensions having pre-release versions in extensions view and editor.", + "Icon used for sponsoring extensions in the extensions view and editor.", + "Full star icon used for the rating in the extensions editor.", + "Half star icon used for the rating in the extensions editor.", + "Empty star icon used for the rating in the extensions editor.", + "Icon shown with a error message in the extensions editor.", + "Icon shown with a warning message in the extensions editor.", + "Icon shown with an info message in the extensions editor.", + "Icon shown with a workspace trust message in the extension editor.", + "Icon shown with a activation time message in the extension editor." + ], + "vs/workbench/contrib/extensions/browser/extensionsViews": [ "Extensions", - "Install Missing Dependencies", - "Finished installing missing dependencies. Please reload the window now.", - "Reload Window", - "There are no missing dependencies to install." + "Unable to search the Marketplace when offline, please check your network connection.", + "Error while fetching extensions. {0}", + "No extensions found.", + "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting.", + "Open User Settings", + "There are no extensions to install.", + "Verified Publisher {0}", + "Publisher {0}", + "Deprecated", + "Rated {0} out of 5 stars by {1} users" + ], + "vs/platform/dnd/browser/dnd": [ + "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again." + ], + "vs/platform/actions/browser/toolbar": [ + "Hide", + "Reset Menu" ], "vs/workbench/contrib/extensions/browser/extensionsActions": [ "{0} for the Web", "The '{0}' extension is not available in {1}. Click 'More Information' to learn more.", "&&More Information", "Close", + "Install Pre-Release", + "Cancel", "{0} cannot verify the '{1}' extension. Are you sure you want to install it?", "Install Anyway", "Cancel", @@ -25398,6 +27577,7 @@ "Are you sure you want to install '{0}'?", "Installing extension {0} started. An editor is now open with more details on this extension", "Installing extension {0} is completed.", + "Install Workspace Extension", "Install Pre-Release", "Install Pre-Release Version", "Install", @@ -25419,17 +27599,22 @@ "Update", "Updating extension {0} to version {1} started.", "Updating extension {0} to version {1} completed.", - "Ignore Updates", - "Ignoring {0} updates", + "Enabled auto updates for", + "Disabled auto updates for", + "Auto Update All (From Publisher)", + "Ignoring updates published by {0}.", + "Enabled auto updates for", + "Disabled auto updates for", "Migrate", "Migrate to {0}", "Migrate", "Manage", "Manage", - "Switch to Pre-Release Version", - "Switch to Pre-Release version of this extension", + "Pre-Release", "Switch to Release Version", - "Switch to Release version of this extension", + "This will switch and enable updates to release versions", + "Switch to Pre-Release Version", + "This will switch to pre-release version and enable updates to latest version always", "Install Another Version...", "This extension has no other versions.", "pre-release", @@ -25445,17 +27630,14 @@ "Disable this extension", "Enable", "Disable", - "Reload", - "Reload Required", + "Reload Window", + "Restart Extensions", + "Restart to Update", + "Update {0}", "current", - "Set Color Theme", "Select Color Theme", - "Set File Icon Theme", "Select File Icon Theme", - "Set Product Icon Theme", "Select Product Icon Theme", - "Set Display Language", - "Clear Display Language", "Show Recommended Extension", "Install Recommended Extension", "Do not recommend this extension again", @@ -25497,6 +27679,7 @@ "Learn More", "This extension is disabled because it is not supported in {0} for the Web.", "Learn More", + "Manage Access", "Install the language pack extension on '{0}' to enable it there also.", "Install the language pack extension locally to enable it there also.", "This extension is enabled in the Remote Extension Host because it prefers to run there.", @@ -25532,7 +27715,79 @@ "Button separator color for extension actions", "Button background color for extension actions that stand out (e.g. install button).", "Button foreground color for extension actions that stand out (e.g. install button).", - "Button background hover color for extension actions that stand out (e.g. install button)." + "Button background hover color for extension actions that stand out (e.g. install button).", + "Auto Update", + "Set Color Theme", + "Set File Icon Theme", + "Set Product Icon Theme", + "Set Display Language", + "Clear Display Language" + ], + "vs/workbench/contrib/extensions/browser/extensionEditor": [ + "Extension Version", + "Extension name", + "Preview", + "Preview", + "Built-in", + "Publisher", + "Install count", + "Rating", + "Workspace Extension", + "Local Extension", + "Details", + "Extension details, rendered from the extension's 'README.md' file", + "Features", + "Lists features contributed by this extension", + "Changelog", + "Extension update history, rendered from the extension's 'CHANGELOG.md' file", + "Dependencies", + "Lists extensions this extension depends on", + "Extension Pack", + "Lists extensions those will be installed together with this extension", + "No README available.", + "Readme", + "Extension Pack ({0})", + "No README available.", + "Readme", + "Categories", + "Marketplace", + "Issues", + "Repository", + "License", + "Resources", + "More Info", + "Published", + "Last released", + "Last updated", + "Identifier", + "No Changelog available.", + "Changelog", + "No Dependencies", + "Find", + "Find Next", + "Find Previous" + ], + "vs/workbench/contrib/extensions/common/extensionsFileTemplate": [ + "Extensions", + "List of extensions which should be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'.", + "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.", + "List of extensions recommended by VS Code that should not be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'.", + "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'." + ], + "vs/workbench/contrib/extensions/browser/extensionsActivationProgress": [ + "Activating Extensions..." + ], + "vs/workbench/contrib/extensions/browser/extensionsDependencyChecker": [ + "Extensions", + "Install Missing Dependencies", + "Finished installing missing dependencies. Please reload the window now.", + "Reload Window", + "There are no missing dependencies to install." + ], + "vs/workbench/contrib/extensions/common/extensionsUtils": [ + "Disable other keymaps ({0}) to avoid conflicts between keybindings?", + "Yes", + "No" ], "vs/workbench/contrib/extensions/browser/extensionsQuickAccess": [ "Type an extension name to install or search.", @@ -25541,7 +27796,6 @@ "Press Enter to manage your extensions." ], "vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService": [ - "Don't Show Again", "Do you want to ignore all extension recommendations?", "Yes, Ignore All", "No", @@ -25552,61 +27806,13 @@ "extensions from {0}", "Do you want to install the recommended {0} for {1}?", "You have {0} installed on your system. Do you want to install the recommended {1} for it?", + "Don't Show Again for this Repository", + "Don't Show Again for these Extensions", + "Don't Show Again for this Extension", "Install", "Install (Do not sync)", "Show Recommendations" ], - "vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant": [ - "Restarting extension host due to workspace trust change." - ], - "vs/workbench/contrib/extensions/browser/extensionsWorkbenchService": [ - "Manifest is not found", - "Please reload Visual Studio Code to complete the uninstallation of this extension.", - "Please reload Visual Studio Code to enable the updated extension.", - "Please reload Visual Studio Code to enable this extension locally.", - "Please reload Visual Studio Code to enable this extension in {0}.", - "Please reload Visual Studio Code to enable this extension.", - "Please reload Visual Studio Code to enable this extension.", - "Please reload Visual Studio Code to disable this extension.", - "Please reload Visual Studio Code to enable this extension.", - "Please reload Visual Studio Code to enable this extension.", - "This extension is reported to be problematic.", - "Can't install '{0}' extension because it is not compatible.", - "Uninstalling extension....", - "Unable to install extension '{0}' because the requested version '{1}' is not found.", - "Installing extension....", - "Installing '{0}' extension....", - "Disable All", - "Cannot disable '{0}' extension alone. '{1}' extension depends on this. Do you want to disable all these extensions?", - "Cannot disable '{0}' extension alone. '{1}' and '{2}' extensions depend on this. Do you want to disable all these extensions?", - "Cannot disable '{0}' extension alone. '{1}', '{2}' and other extensions depend on this. Do you want to disable all these extensions?" - ], - "vs/workbench/contrib/extensions/browser/extensionsIcons": [ - "View icon of the extensions view.", - "Icon for the 'Manage' action in the extensions view.", - "Icon for the 'Clear Search Result' action in the extensions view.", - "Icon for the 'Refresh' action in the extensions view.", - "Icon for the 'Filter' action in the extensions view.", - "Icon for the 'Install Local Extension in Remote' action in the extensions view.", - "Icon for the 'Install Workspace Recommended Extensions' action in the extensions view.", - "Icon for the 'Configure Recommended Extensions' action in the extensions view.", - "Icon to indicate that an extension is synced.", - "Icon to indicate that an extension is ignored when syncing.", - "Icon to indicate that an extension is remote in the extensions view and editor.", - "Icon shown along with the install count in the extensions view and editor.", - "Icon shown along with the rating in the extensions view and editor.", - "Icon used for the verified extension publisher in the extensions view and editor.", - "Icon shown for extensions having pre-release versions in extensions view and editor.", - "Icon used for sponsoring extensions in the extensions view and editor.", - "Full star icon used for the rating in the extensions editor.", - "Half star icon used for the rating in the extensions editor.", - "Empty star icon used for the rating in the extensions editor.", - "Icon shown with a error message in the extensions editor.", - "Icon shown with a warning message in the extensions editor.", - "Icon shown with an info message in the extensions editor.", - "Icon shown with a workspace trust message in the extension editor.", - "Icon shown with a activation time message in the extension editor." - ], "vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor": [ "Activated by {0} on start-up", "Activated by {1} because a file matching {0} exists in your workspace", @@ -25618,12 +27824,18 @@ "Extension is activating...", "Extension has caused the extension host to freeze.", "{0} uncaught errors", + "{0} Requests: {1} (Overall)", + ", {0} (Session)", + "Last request was {0}.", "Runtime Extensions", "Copy id ({0})", "Disable (Workspace)", "Disable", "Show Running Extensions" ], + "vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant": [ + "Restarting extension host due to workspace trust change." + ], "vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider": [ "Example" ], @@ -25632,34 +27844,53 @@ "Show Deprecated Extensions", "Don't Show Again" ], - "vs/workbench/contrib/extensions/browser/extensionsViews": [ - "Extensions", - "Unable to search the Marketplace when offline, please check your network connection.", - "Error while fetching extensions. {0}", - "No extensions found.", - "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting.", - "Open User Settings", - "There are no extensions to install.", - "Verified Publisher {0}", - "Publisher {0}", - "Deprecated", - "Rated {0} out of 5 stars by {1} users" - ], - "vs/platform/dnd/browser/dnd": [ - "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again." + "vs/workbench/contrib/extensions/browser/extensionsWorkbenchService": [ + "Manifest is not found", + "Enable or Disable extensions", + "reload window", + "restart extensions", + "Please {0} to complete the uninstallation of this extension.", + "Please update {0} to enable the updated extension.", + "Please update {0} to enable the updated extension.", + "Please restart {0} to enable the updated extension.", + "Please {0} to enable the updated extension.", + "Please {0} to enable this extension locally.", + "Please {0} to enable this extension in {1}.", + "Please {0} to enable this extension.", + "Please {0} to enable this extension.", + "Please {0} to disable this extension.", + "Please {0} to enable this extension.", + "Please {0} to enable this extension.", + "This extension is reported to be problematic.", + "Unable to install extension '{0}' because the requested version '{1}' is not found.", + "Unable to install extension '{0}' because it is not found.", + "&&Install Extension", + "&&Install Extension and {0}", + "Open Extension", + "Install Extension", + "Would you like to install '{0}' extension from '{1}'?", + "Would you like to install the extension?", + "Sync this extension", + "Unable to install extension", + "Enable Extension", + "Would you like to enable '{0}' extension?", + "&&Enable Extension", + "&&Enable Extension and {0}", + "Can't install '{0}' extension because it is not compatible.", + "Uninstalling extension....", + "Installing '{0}' extension....", + "Installing extension....", + "Disable All", + "Cannot disable '{0}' extension alone. '{1}' extension depends on this. Do you want to disable all these extensions?", + "Cannot disable '{0}' extension alone. '{1}' and '{2}' extensions depend on this. Do you want to disable all these extensions?", + "Cannot disable '{0}' extension alone. '{1}', '{2}' and other extensions depend on this. Do you want to disable all these extensions?" ], "vs/workbench/contrib/terminal/browser/terminal.contribution": [ "Type the name of a terminal to open.", "Show All Opened Terminals", + "&&Terminal", "Terminal", - "Terminal", - "&&Terminal" - ], - "vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution": [ - "Show Terminal Texture Atlas", - "Write Data to Terminal", - "Enter data to write directly to the terminal, bypassing the pty", - "Restart Pty Host" + "Terminal" ], "vs/workbench/contrib/terminal/browser/terminalView": [ "Use 'monospace'", @@ -25667,22 +27898,40 @@ "Open Terminals.", "Starting..." ], - "vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution": [ - "Focus Accessible Buffer", - "Accessible Buffer Go to Next Command", - "Accessible Buffer Go to Previous Command" + "vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution": [ + "Enter data to write directly to the terminal, bypassing the pty", + "Terminal Dev Mode", + "Show Terminal Texture Atlas", + "Write Data to Terminal", + "Restart Pty Host" ], "vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution": [ - "Show Environment Contributions", "Terminal Environment Changes", "Extension: {0}", - "workspace" + "workspace", + "Show Environment Contributions" + ], + "vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution": [ + "Focus Accessible Terminal View", + "Accessible Buffer Go to Next Command", + "Accessible Buffer Go to Previous Command", + "Scroll to Accessible View Bottom", + "Scroll to Accessible View Top" ], "vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution": [ "Open Detected Link...", "Open Last URL Link", + "Opens the last detected URL/URI link in the terminal", "Open Last Local File Link" ], + "vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution": [ + "Show Terminal Quick Fixes" + ], + "vs/workbench/contrib/terminalContrib/zoom/browser/terminal.zoom.contribution": [ + "Increase Font Size", + "Decrease Font Size", + "Reset Font Size" + ], "vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution": [ "Focus Find", "Hide Find", @@ -25693,8 +27942,13 @@ "Find Previous", "Search Workspace" ], - "vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution": [ - "Show Terminal Quick Fixes" + "vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution": [ + "Select the Previous Suggestion", + "Select the Previous Page Suggestion", + "Select the Next Suggestion", + "Select the Next Page Suggestion", + "Accept Selected Suggestion", + "Hide Suggest Widget" ], "vs/workbench/contrib/tasks/browser/runAutomaticTasks": [ "Manage Automatic Tasks", @@ -25771,6 +28025,16 @@ "ESLint stylish problems", "Go problems" ], + "vs/workbench/contrib/tasks/common/jsonSchema_v1": [ + "Task version 0.1.0 is deprecated. Please use 2.0.0", + "The config's version number", + "The runner has graduated. Use the official runner property", + "Defines whether the task is executed as a process and the output is shown in the output window or inside the terminal.", + "Windows specific command configuration", + "Mac specific command configuration", + "Linux specific command configuration", + "Specifies whether the command is a shell command or an external program. Defaults to false if omitted." + ], "vs/workbench/contrib/tasks/common/jsonSchema_v2": [ "Specifies whether the command is a shell command or an external program. Defaults to false if omitted.", "The property isShellCommand is deprecated. Use the type property of the task and the shell property in the options instead. See also the 1.14 release notes.", @@ -25866,38 +28130,18 @@ "The task type configuration is missing the required 'taskType' property", "Contributes task kinds" ], - "vs/workbench/contrib/tasks/common/jsonSchema_v1": [ - "Task version 0.1.0 is deprecated. Please use 2.0.0", - "The config's version number", - "The runner has graduated. Use the official runner property", - "Defines whether the task is executed as a process and the output is shown in the output window or inside the terminal.", - "Windows specific command configuration", - "Mac specific command configuration", - "Linux specific command configuration", - "Specifies whether the command is a shell command or an external program. Defaults to false if omitted." - ], "vs/workbench/contrib/remote/browser/tunnelFactory": [ "Private", "Public" ], "vs/workbench/contrib/remote/browser/remote": [ - "The ID of a Get Started walkthrough to open.", - "Contributes help information for Remote", - "The url, or a command that returns the url, to your project's Getting Started page, or a walkthrough ID contributed by your project's extension", - "The url, or a command that returns the url, to your project's documentation page", - "The url, or a command that returns the url, to your project's feedback reporter", - "Use {0} instead", - "The url, or a command that returns the url, to your project's issue reporter", - "The url, or a command that returns the url, to your project's issues list", "Get Started", "Read Documentation", "Review Issues", "Report Issue", "Select url to open", - "Help and feedback", "Remote Help", "Remote Explorer", - "Remote Explorer", "Attempting to reconnect in {0} second...", "Attempting to reconnect in {0} seconds...", "Reconnect Now", @@ -25905,18 +28149,12 @@ "Connection Lost", "Disconnected. Attempting to reconnect...", "Cannot reconnect. Please reload the window.", - "&&Reload Window" - ], - "vs/workbench/contrib/emmet/browser/actions/expandAbbreviation": [ - "Emmet: Expand Abbreviation", - "Emmet: E&&xpand Abbreviation" + "&&Reload Window", + "Help and feedback", + "Remote Explorer" ], "vs/workbench/contrib/remote/browser/remoteIndicator": [ - "Remote", - "Show Remote Menu", - "Close Remote Connection", "Close Re&&mote Connection", - "Install Remote Development Extensions", "Opening Remote...", "Opening Remote...", "Reconnecting to {0}...", @@ -25934,94 +28172,39 @@ "Reload Window", "Close Remote Workspace", "Select an option to open a Remote Window", - "Installing extension... " - ], - "vs/workbench/contrib/format/browser/formatActionsNone": [ - "Format Document", - "This file cannot be formatted because it is too large", - "There is no formatter for '{0}' files installed.", - "&&Install Formatter..." - ], - "vs/workbench/contrib/format/browser/formatModified": [ - "Format Modified Lines" - ], - "vs/workbench/contrib/snippets/browser/commands/configureSnippets": [ - "(global)", - "({0})", - "({0}) {1}", - "Type snippet file name", - "Invalid file name", - "'{0}' is not a valid file name", - "'{0}' already exists", - "Configure User Snippets", - "User Snippets", - "User &&Snippets", - "global", - "New Global Snippets file...", - "{0} workspace", - "New Snippets file for '{0}'...", - "Existing Snippets", - "New Snippets", - "New Snippets", - "Select Snippets File or Create Snippets" - ], - "vs/workbench/contrib/format/browser/formatActionsMultiple": [ - "None", - "None", - "Extension '{0}' is configured as formatter but it cannot format '{1}'-files", - "There are multiple formatters for '{0}' files. One of them should be configured as default formatter.", - "Extension '{0}' is configured as formatter but not available. Select a different default formatter to continue.", - "Configure Default Formatter", - "&&Configure...", - "Configure...", - "Select a default formatter for '{0}' files", - "Configure...", - "Formatter Conflicts", - "Formatting", - "Defines a default formatter which takes precedence over all other formatter settings. Must be the identifier of an extension contributing a formatter.", - "(default)", - "Configure Default Formatter...", - "Select a formatter", - "Select a default formatter for '{0}' files", - "Format Document With...", - "Format Selection With..." - ], - "vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets": [ - "Fill File with Snippet", - "Select a snippet" - ], - "vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet": [ - "Surround With Snippet..." - ], - "vs/workbench/contrib/snippets/browser/commands/insertSnippet": [ - "Insert Snippet" + "Installing extension... ", + "When enabled, remote extensions recommendations will be shown in the Remote Indicator menu.", + "Remote", + "Show Remote Menu", + "Close Remote Connection", + "Install Remote Development Extensions" ], - "vs/workbench/contrib/snippets/browser/snippetCodeActionProvider": [ - "Surround With: {0}", - "Start with Snippet", - "Start with: {0}" + "vs/workbench/contrib/remote/browser/remoteConnectionHealth": [ + "You are about to connect to an OS version that is unsupported by {0}.", + "&&Allow", + "&&Learn More", + "Do not show again", + "Learn More", + "You are connected to an OS version that is unsupported by {0}." ], - "vs/workbench/contrib/snippets/browser/snippetsService": [ - "Expected string in `contributes.{0}.path`. Provided value: {1}", - "When omitting the language, the value of `contributes.{0}.path` must be a `.code-snippets`-file. Provided value: {1}", - "Unknown language in `contributes.{0}.language`. Provided value: {1}", - "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", - "Contributes snippets.", - "Language identifier for which this snippet is contributed to.", - "Path of the snippets file. The path is relative to the extension folder and typically starts with './snippets/'.", - "One or more snippets from the extension '{0}' very likely confuse snippet-variables and snippet-placeholders (see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax for more details)", - "The snippet file \"{0}\" could not be read." + "vs/workbench/contrib/emmet/browser/actions/expandAbbreviation": [ + "Emmet: Expand Abbreviation", + "Emmet: E&&xpand Abbreviation" ], "vs/workbench/contrib/codeEditor/browser/accessibility/accessibility": [ - "Toggle Screen Reader Accessibility Mode" + "Toggle Screen Reader Accessibility Mode", + "Toggles an optimized mode for usage with screen readers, braille devices, and other assistive technologies." ], "vs/workbench/contrib/codeEditor/browser/diffEditorHelper": [ "Show Whitespace Differences", "The diff algorithm was stopped early (after {0} ms.)", "Remove Limit", + "Run the command Diff Editor: Switch Side ({0}) to toggle between the original and modified editors.", + "Run the command Diff Editor: Switch Side, which is currently not triggerable via keybinding, to toggle between the original and modified editors.", + "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor.", "You are in a diff editor.", "View the next ({0}) or previous ({1}) diff in diff review mode, which is optimized for screen readers.", - "To control which audio cues should be played, the following settings can be configured: {0}." + "To control which accessibility signals should be played, the following settings can be configured: {0}." ], "vs/workbench/contrib/codeEditor/browser/inspectKeybindings": [ "Inspect Key Mappings", @@ -26037,9 +28220,9 @@ "Loading..." ], "vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess": [ - "Go to Line/Column...", "Type the line number and optional column to go to (e.g. 42:5 for line 42 and column 5).", - "Go to Line/Column" + "Go to Line/Column", + "Go to Line/Column..." ], "vs/workbench/contrib/codeEditor/browser/saveParticipants": [ "Running '{0}' Formatter ([configure]({1})).", @@ -26048,26 +28231,26 @@ "Applying code action '{0}'." ], "vs/workbench/contrib/codeEditor/browser/toggleColumnSelection": [ - "Toggle Column Selection Mode", - "Column &&Selection Mode" + "Column &&Selection Mode", + "Toggle Column Selection Mode" ], "vs/workbench/contrib/codeEditor/browser/toggleMinimap": [ - "Toggle Minimap", - "&&Minimap" + "&&Minimap", + "Toggle Minimap" ], - "vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace": [ - "Toggle Render Whitespace", - "&&Render Whitespace" + "vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter": [ + "Render &&Control Characters", + "Toggle Control Characters" ], "vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier": [ - "Toggle Multi-Cursor Modifier", "Switch to Alt+Click for Multi-Cursor", "Switch to Cmd+Click for Multi-Cursor", - "Switch to Ctrl+Click for Multi-Cursor" + "Switch to Ctrl+Click for Multi-Cursor", + "Toggle Multi-Cursor Modifier" ], - "vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter": [ - "Toggle Control Characters", - "Render &&Control Characters" + "vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace": [ + "&&Render Whitespace", + "Toggle Render Whitespace" ], "vs/workbench/contrib/codeEditor/browser/toggleWordWrap": [ "Whether the editor is currently using word wrapping.", @@ -26076,13 +28259,119 @@ "Enable wrapping for this file", "&&Word Wrap" ], - "vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint": [ - "Press {0} to ask {1} to do something. ", - "Start typing to dismiss.", - "[[Ask {0} to do something]] or start typing to dismiss.", - "[[Select a language]], or [[fill with template]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don't show]] this again.", - "Execute {0} to select a language, execute {1} to fill with template, or execute {2} to open a different editor and get started. Start typing to dismiss.", - " Toggle {0} in settings to disable this hint." + "vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint": [ + "Press {0} to ask {1} to do something. ", + "Start typing to dismiss.", + "[[Ask {0} to do something]] or start typing to dismiss.", + "[[Select a language]], or [[fill with template]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don't show]] this again.", + "Execute {0} to select a language, execute {1} to fill with template, or execute {2} to open a different editor and get started. Start typing to dismiss.", + " Toggle {0} in settings to disable this hint." + ], + "vs/workbench/contrib/codeEditor/browser/dictation/editorDictation": [ + "Stop Dictation ({0})", + "Stop Dictation", + "Voice", + "Start Dictation in Editor", + "Stop Dictation in Editor" + ], + "vs/platform/history/browser/contextScopedHistoryWidget": [ + "Whether suggestion are visible" + ], + "vs/workbench/contrib/debug/browser/linkDetector": [ + "follow link using forwarded port", + "follow link", + "Cmd + click to {0}\n{1}", + "Ctrl + click to {0}\n{1}", + "Cmd + click to {0}", + "Ctrl + click to {0}" + ], + "vs/workbench/contrib/debug/browser/replViewer": [ + "Debug Console", + "Variable {0}, value {1}", + ", occurred {0} times", + "Debug console variable {0}, value {1}", + "Debug console group {0}" + ], + "vs/workbench/contrib/debug/common/replModel": [ + "Console was cleared" + ], + "vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet": [ + "Surround with Snippet..." + ], + "vs/workbench/contrib/snippets/browser/commands/configureSnippets": [ + "(global)", + "({0})", + "({0}) {1}", + "Type snippet file name", + "Invalid file name", + "'{0}' is not a valid file name", + "'{0}' already exists", + "User &&Snippets", + "global", + "New Global Snippets file...", + "{0} workspace", + "New Snippets file for '{0}'...", + "Existing Snippets", + "New Snippets", + "New Snippets", + "Select Snippets File or Create Snippets", + "Configure User Snippets", + "User Snippets" + ], + "vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets": [ + "Select a snippet", + "Fill File with Snippet" + ], + "vs/workbench/contrib/snippets/browser/snippetCodeActionProvider": [ + "More...", + "{0}", + "Start with Snippet", + "Start with: {0}" + ], + "vs/workbench/contrib/snippets/browser/commands/insertSnippet": [ + "Insert Snippet" + ], + "vs/workbench/contrib/snippets/browser/snippetsService": [ + "Expected string in `contributes.{0}.path`. Provided value: {1}", + "When omitting the language, the value of `contributes.{0}.path` must be a `.code-snippets`-file. Provided value: {1}", + "Unknown language in `contributes.{0}.language`. Provided value: {1}", + "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", + "Contributes snippets.", + "Language identifier for which this snippet is contributed to.", + "Path of the snippets file. The path is relative to the extension folder and typically starts with './snippets/'.", + "One or more snippets from the extension '{0}' very likely confuse snippet-variables and snippet-placeholders (see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax for more details)", + "The snippet file \"{0}\" could not be read." + ], + "vs/workbench/contrib/format/browser/formatActionsNone": [ + "Format Document", + "This file cannot be formatted because it is too large", + "There is no formatter for '{0}' files installed.", + "&&Install Formatter..." + ], + "vs/workbench/contrib/format/browser/formatActionsMultiple": [ + "None", + "None", + "Extension '{0}' is configured as formatter but it cannot format '{1}'-files", + "Extension '{0}' is configured as formatter but it can only format '{1}'-files as a whole, not selections or parts of it.", + "There are multiple formatters for '{0}' files. One of them should be configured as default formatter.", + "Extension '{0}' is configured as formatter but not available. Select a different default formatter to continue.", + "Configure Default Formatter", + "&&Configure...", + "Configure...", + "Select a default formatter for '{0}' files", + "Configure...", + "Formatter Conflicts", + "Formatting", + "Defines a default formatter which takes precedence over all other formatter settings. Must be the identifier of an extension contributing a formatter.", + "(default)", + "Configure Default Formatter...", + "Select a formatter", + "Select a default formatter for '{0}' files", + "Format Document With...", + "Format Selection With..." + ], + "vs/workbench/contrib/format/browser/formatModified": [ + "Format Modified Lines" ], "vs/workbench/contrib/update/browser/update": [ "This version of {0} does not have release notes online", @@ -26127,6 +28416,12 @@ "&&Insiders", "&&Stable (current)" ], + "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService": [ + "Built-In", + "Developer", + "Reset Welcome Page Walkthrough Progress", + "Reset the progress of all Walkthrough steps on the Welcome Page to make them appear as if they are being viewed for the first time, providing a fresh start to the getting started experience." + ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput": [ "Welcome" ], @@ -26138,15 +28433,6 @@ "Used to represent walkthrough steps which have not been completed", "Used to represent walkthrough steps which have been completed" ], - "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService": [ - "Built-In", - "Developer", - "Reset Welcome Page Walkthrough Progress" - ], - "vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart": [ - "unbound", - "It looks like Git is not installed on your system." - ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted": [ "Overview of how to get up to speed with your editor.", "Open Walkthrough...", @@ -26170,6 +28456,9 @@ "More...", "Hide", "Hide", + "Videos", + "Watch Getting Started Tutorials", + "Learn VS Code's must-have features in short and practical videos", "All {0} steps complete!", "{0} of {1} steps complete", "Tip: Use keyboard shortcut ", @@ -26182,7 +28471,12 @@ ], "vs/workbench/contrib/welcomeWalkthrough/browser/editor/editorWalkThrough": [ "Editor Playground", - "Interactive Editor Playground" + "Interactive Editor Playground", + "Opens an interactive playground for learning about the editor." + ], + "vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart": [ + "unbound", + "It looks like Git is not installed on your system." ], "vs/workbench/contrib/welcomeViews/common/viewsWelcomeContribution": [ "The viewsWelcome contribution in '{0}' requires 'enabledApiProposals: [\"contribViewsWelcome\"]' in order to use the 'group' proposed property." @@ -26197,11 +28491,12 @@ "Group to which this welcome content belongs. Proposed API.", "Condition when the welcome content buttons and command links should be enabled." ], - "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree": [ - "{0} ({1})", - "1 problem in this element", - "{0} problems in this element", - "Contains elements with problems" + "vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek": [ + "Calls from '{0}'", + "Callers of '{0}'", + "Loading...", + "No calls from '{0}'", + "No callers of '{0}'" ], "vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek": [ "Supertypes of '{0}'", @@ -26210,12 +28505,11 @@ "No supertypes of '{0}'", "No subtypes of '{0}'" ], - "vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek": [ - "Calls from '{0}'", - "Callers of '{0}'", - "Loading...", - "No calls from '{0}'", - "No callers of '{0}'" + "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree": [ + "{0} ({1})", + "1 problem in this element", + "{0} problems in this element", + "Contains elements with problems" ], "vs/workbench/contrib/outline/browser/outlinePane": [ "The active editor cannot provide outline information.", @@ -26231,50 +28525,28 @@ "Sort By: Name", "Sort By: Category" ], - "vs/workbench/contrib/userDataProfile/browser/userDataProfileActions": [ - "Create a Temporary Profile", - "Rename...", - "Rename {0}", - "Profile with name {0} already exists.", - "Current", - "Rename Profile...", - "Select Profile to Rename", - "Manage...", - "Cleanup Profiles", - "Reset Workspace Profiles Associations" + "vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction": [ + "Sign out of account", + "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", + "Sign out of '{0}'?", + "&&Sign Out" ], - "vs/workbench/contrib/userDataProfile/browser/userDataProfile": [ - "Profiles ({0})", - "Switch Profile...", - "Select Profile", - "Edit Profile...", - "Show Profile Contents", - "Export Profile...", - "Export Profile ({0})...", - "Import Profile...", - "Import from URL", - "Select File...", - "Profile Templates", - "Import from Profile Template...", - "Provide Profile Template URL", - "Error while creating profile: {0}", - "Select Profile Template File", - "Import Profile...", - "Save Current Profile As...", - "Create Profile...", - "Delete Profile...", - "Current", - "Delete Profile...", - "Select Profiles to Delete" + "vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction": [ + "Pick an account to manage trusted extensions for", + "This account has not been used by any extensions.", + "Cancel", + "Last used this account {0}", + "Has not used this account", + "This extension is trusted by Microsoft and\nalways has access to this account", + "Trusted by Microsoft", + "Manage Trusted Extensions", + "Choose which extensions can access this account", + "Manage Trusted Extensions For Account", + "Accounts" ], "vs/workbench/contrib/userDataSync/browser/userDataSync": [ - "Turn Off", - "Configure...", - "Sync Now", "syncing", "synced {0}", - "Show Settings", - "Show Synced Data", "Unable to sync due to conflicts in {0}. Please resolve them to continue.", "Replace Remote", "Replace Local", @@ -26331,12 +28603,10 @@ "Default", "Insiders", "Stable", - "Backup and Sync Settings...", "Turning on Settings Sync...", "Cancel", "Sign in to Sync Settings", "Sign in to Sync Settings (1)", - "Show Conflicts ({0})", "Settings Sync is On", "Error while turning off Settings Sync. Please check [logs]({0}) for more details.", "Configure...", @@ -26344,50 +28614,38 @@ "Show Log", "Complete Merge", "Successfully downloaded Settings Sync activity.", - "Clear Data in Cloud..." - ], - "vs/workbench/contrib/editSessions/common/editSessions": [ - "Cloud Changes", - "Cloud Changes", - "View icon of the cloud changes view." - ], - "vs/workbench/contrib/editSessions/common/editSessionsLogService": [ - "Cloud Changes" - ], - "vs/workbench/contrib/editSessions/browser/editSessionsStorageService": [ - "Select an account to restore your working changes from the cloud", - "Select an account to store your working changes in the cloud", - "Signed In", - "Others", - "Sign in with {0}", - "Turn on Cloud Changes...", - "Turn on Cloud Changes... (1)", - "Turn off Cloud Changes...", - "Do you want to disable storing working changes in the cloud?", - "Delete all stored data from the cloud." + "Clear Data in Cloud...", + "Turn Off", + "Configure...", + "Sync Now", + "Show Settings", + "Show Synced Data", + "Backup and Sync Settings...", + "Show Conflicts ({0})" ], - "vs/workbench/contrib/editSessions/browser/editSessionsViews": [ - "You have no stored changes in the cloud to display.\n{0}", - "Store Working Changes", - "Resume Working Changes", - "Store Working Changes", - "Delete Working Changes", - "Are you sure you want to permanently delete your working changes with ref {0}?", - " You cannot undo this action.", - "Delete All Working Changes from Cloud", - "Are you sure you want to permanently delete all stored changes from the cloud?", - " You cannot undo this action.", - "Compare Changes", - "Local Copy", - "Cloud Changes", - "Open File" + "vs/workbench/contrib/userDataProfile/browser/userDataProfileActions": [ + "Rename {0}", + "Profile with name {0} already exists.", + "Current", + "Rename Profile...", + "Select Profile to Rename", + "Create a Temporary Profile", + "Rename...", + "Manage...", + "Cleanup Profiles", + "Reset Workspace Profiles Associations" ], "vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint": [ "Configure which editor to use for a resource.", "Language modes that the code actions are enabled for.", "`CodeActionKind` of the contributed code action.", "Label for the code action used in the UI.", - "Description of what the code action does." + "Description of what the code action does.", + "Title", + "Kind", + "Description", + "Languages", + "Code Actions" ], "vs/workbench/contrib/codeActions/common/documentationExtensionPoint": [ "Contributed documentation.", @@ -26397,20 +28655,9 @@ "When clause.", "Command executed." ], - "vs/workbench/contrib/codeActions/browser/codeActionsContribution": [ - "Triggers Code Actions on explicit saves and auto saves triggered by window or focus changes.", - "Triggers Code Actions only when explicitly saved", - "Never triggers Code Actions on save", - "Triggers Code Actions only when explicitly saved. This value will be deprecated in favor of \"explicit\".", - "Never triggers Code Actions on save. This value will be deprecated in favor of \"never\".", - "Controls whether auto fix action should be run on file save.", - "Run CodeActions for the editor on save. CodeActions must be specified and the editor must not be shutting down. Example: `\"source.organizeImports\": \"explicit\" `", - "Controls whether '{0}' actions should be run on file save." - ], "vs/workbench/contrib/timeline/browser/timelinePane": [ "Loading...", "Load more", - "Timeline", "The active editor cannot provide timeline information.", "All timeline sources have been filtered out.", "Local History will track recent changes as you save them unless the file has been excluded or is too large.", @@ -26424,6 +28671,7 @@ "Icon for the refresh timeline action.", "Icon for the pin timeline action.", "Icon for the unpin timeline action.", + "Timeline", "Refresh", "Timeline", "Pin the Current Timeline", @@ -26431,53 +28679,129 @@ "Unpin the Current Timeline", "Timeline" ], + "vs/workbench/contrib/codeActions/browser/codeActionsContribution": [ + "Triggers Code Actions on explicit saves and auto saves triggered by window or focus changes.", + "Triggers Code Actions only when explicitly saved", + "Never triggers Code Actions on save", + "Triggers Code Actions only when explicitly saved. This value will be deprecated in favor of \"explicit\".", + "Never triggers Code Actions on save. This value will be deprecated in favor of \"never\".", + "Controls whether auto fix action should be run on file save.", + "Run Code Actions for the editor on save. Code Actions must be specified and the editor must not be shutting down. Example: `\"source.organizeImports\": \"explicit\" `", + "Controls whether '{0}' actions should be run on file save." + ], "vs/workbench/contrib/localHistory/browser/localHistoryTimeline": [ "Local History" ], + "vs/workbench/contrib/userDataProfile/browser/userDataProfile": [ + "Profiles ({0})", + "Select Profile", + "Import from URL", + "Select File...", + "Profile Templates", + "Import from Profile Template...", + "Provide Profile Template URL", + "Error while creating profile: {0}", + "Select Profile Template File", + "Current", + "Delete Profile...", + "Select Profiles to Delete", + "Switch Profile...", + "Edit Profile...", + "Show Profile Contents", + "Export Profile...", + "Export Profile ({0})...", + "Import Profile...", + "Import Profile...", + "Save Current Profile As...", + "Create Profile...", + "Delete Profile..." + ], "vs/workbench/contrib/localHistory/browser/localHistoryCommands": [ - "Local History", - "Compare with File", - "Compare with Previous", - "Select for Compare", - "Compare with Selected", - "Show Contents", - "Restore Contents", "File Restored", "Do you want to restore the contents of '{0}'?", "Restoring will discard any unsaved changes.", "&&Restore", "Unable to restore '{0}'.", - "Find Entry to Restore", "Select the file to show local history for", "Select the local history entry to open", - "Local History: Find Entry to Restore...", - "Rename", "Rename Local History Entry", "Enter the new name of the local history entry", - "Delete", "Do you want to delete the local history entry of '{0}' from {1}?", "This action is irreversible!", "&&Delete", - "Delete All", "Do you want to delete all entries of all files in local history?", "This action is irreversible!", "&&Delete All", - "Create Entry", "Create Local History Entry", "Enter the new name of the local history entry for '{0}'", "{0} ({1} • {2})", "{0} ({1} • {2}) ↔ {3}", - "{0} ({1} • {2}) ↔ {3} ({4} • {5})" + "{0} ({1} • {2}) ↔ {3} ({4} • {5})", + "Local History", + "Compare with File", + "Compare with Previous", + "Select for Compare", + "Compare with Selected", + "Show Contents", + "Restore Contents", + "Find Entry to Restore", + "Local History: Find Entry to Restore...", + "Rename", + "Delete", + "Delete All", + "Create Entry" + ], + "vs/workbench/contrib/editSessions/common/editSessions": [ + "View icon of the cloud changes view.", + "Cloud Changes", + "Cloud Changes" + ], + "vs/workbench/contrib/editSessions/browser/editSessionsStorageService": [ + "Select an account to restore your working changes from the cloud", + "Select an account to store your working changes in the cloud", + "Signed In", + "Others", + "Sign in with {0}", + "Turn on Cloud Changes...", + "Turn on Cloud Changes... (1)", + "Turn off Cloud Changes...", + "Do you want to disable storing working changes in the cloud?", + "Delete all stored data from the cloud." + ], + "vs/workbench/contrib/editSessions/common/editSessionsLogService": [ + "Cloud Changes" + ], + "vs/workbench/contrib/editSessions/browser/editSessionsViews": [ + "You have no stored changes in the cloud to display.\n{0}", + "Store Working Changes", + "Resume Working Changes", + "Store Working Changes", + "Delete Working Changes", + "Are you sure you want to permanently delete your working changes with ref {0}?", + " You cannot undo this action.", + "Delete All Working Changes from Cloud", + "Are you sure you want to permanently delete all stored changes from the cloud?", + " You cannot undo this action.", + "Compare Changes", + "Local Copy", + "Cloud Changes", + "Open File" + ], + "vs/workbench/contrib/accessibilitySignals/browser/commands": [ + "List all accessibility sounds, noises, or audio cues and configure their settings", + "Configure Sound", + "Select a sound to play and configure", + "List all accessibility announcements, alerts, braille messages, and configure their settings", + "Configure Announcement", + "Select an announcement to configure", + "Screen reader is not active, announcements are disabled by default.", + "Help: List Signal Sounds", + "Help: List Signal Announcements" ], "vs/workbench/services/workspaces/browser/workspaceTrustEditorInput": [ + "Icon of the workspace trust editor label.", "Workspace Trust" ], - "vs/workbench/contrib/audioCues/browser/commands": [ - "Help: List Audio Cues", - "Disabled", - "Enable/Disable Audio Cue", - "Select an audio cue to play" - ], "vs/workbench/contrib/workspace/browser/workspaceTrustEditor": [ "Icon for workspace trust ion the banner.", "Icon for the checkmark in the workspace trust editor.", @@ -26551,22 +28875,6 @@ "This folder is trusted via the bolded entries in the the trusted folders below.", "This window is trusted by nature of the workspace that is opened." ], - "vs/workbench/contrib/accessibility/browser/accessibilityConfiguration": [ - "Accessibility", - "Provide information about how to access the terminal accessibility help menu when the terminal is focused", - "Provide information about how to navigate changes in the diff editor when it is focused", - "Provide information about how to access the chat help menu when the chat input is focused", - "Provide information about how to access the inline editor chat accessibility help menu and alert with hints which describe how to use the feature when the input is focused", - "Provide information about how to access the inline completions hover and accessible view", - "Provide information about how to change a keybinding in the keybindings editor when a row is focused", - "Provide information about how to focus the cell container or inner editor when a notebook cell is focused.", - "Provide information about how to open the hover in an accessible view.", - "Provide information about how to open the notification in an accessible view.", - "Provide information about relevant actions in an empty text editor.", - "Provide information about actions that can be taken in the comment widget or in a file which contains comments.", - "Whether to dim unfocused editors and terminals, which makes it more clear where typed input will go to. This works with the majority of editors with the notable exceptions of those that utilize iframes like notebooks and extension webview editors.", - "The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will only take effect when {0} is enabled." - ], "vs/workbench/contrib/accessibility/browser/accessibilityStatus": [ "Are you using a screen reader to operate VS Code?", "Yes", @@ -26574,14 +28882,105 @@ "Screen Reader Optimized", "Screen Reader Mode" ], + "vs/workbench/contrib/comments/browser/commentsAccessibility": [ + "The editor contains commentable range(s). Some useful commands include:", + "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled ({0}).", + "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus, which is currently not triggerable via keybinding.", + "Some useful comment commands include:", + "- Dismiss Comment (Escape)", + "- Go to Next Commenting Range ({0})", + "- Go to Next Commenting Range, which is currently not triggerable via keybinding.", + "- Go to Previous Commenting Range ({0})", + "- Go to Previous Commenting Range, which is currently not triggerable via keybinding.", + "- Go to Next Comment Thread ({0})", + "- Go to Next Comment Thread, which is currently not triggerable via keybinding.", + "- Go to Previous Comment Thread ({0})", + "- Go to Previous Comment Thread, which is currently not triggerable via keybinding.", + "- Add Comment ({0})", + "- Add Comment on Current Selection, which is currently not triggerable via keybinding.", + "- Submit Comment ({0})", + "- Submit Comment, accessible via tabbing, as it's currently not triggerable with a keybinding." + ], + "vs/workbench/contrib/accessibilitySignals/browser/openDiffEditorAnnouncement": [ + "Diff editor" + ], + "vs/workbench/contrib/accessibility/browser/audioCueConfiguration": [ + "Enable audio cue when a screen reader is attached.", + "Enable audio cue.", + "Disable audio cue.", + "This setting is deprecated. Use `signals` settings instead.", + "Whether or not position changes should be debounced", + "This setting is deprecated, instead use the `signals.debouncePositionChanges` setting.", + "Plays a sound when the active line has a breakpoint.", + "Plays a sound when the active line has an inline suggestion.", + "Plays a sound when the active line has an error.", + "Plays a sound when the active line has a folded area that can be unfolded.", + "Plays a sound when the active line has a warning.", + "Plays a sound when the debugger stopped on a breakpoint.", + "Plays a sound when trying to read a line with inlay hints that has no inlay hints.", + "Plays a sound when a task is completed.", + "Plays a sound when a task fails (non-zero exit code).", + "Plays a sound when a terminal command fails (non-zero exit code).", + "Plays a sound when terminal Quick Fixes are available.", + "Plays a sound when the terminal bell is ringing.", + "Plays a sound when the focus moves to an inserted line in Accessible Diff Viewer mode or to the next/previous change.", + "Plays a sound when the focus moves to a deleted line in Accessible Diff Viewer mode or to the next/previous change.", + "Plays a sound when the focus moves to a modified line in Accessible Diff Viewer mode or to the next/previous change.", + "Plays a sound when a notebook cell execution is successfully completed.", + "Plays a sound when a notebook cell execution fails.", + "Plays a sound when a chat request is made.", + "Plays a sound on loop while the response is pending.", + "Plays a sound on loop while the response has been received.", + "Plays a sound when a feature is cleared (for example, the terminal, Debug Console, or Output channel). When this is disabled, an ARIA alert will announce 'Cleared'.", + "Plays a sound when a file is saved. Also see {0}", + "Plays the audio cue when a user explicitly saves a file.", + "Plays the audio cue whenever a file is saved, including auto save.", + "Never plays the audio cue.", + "Plays a sound when a file or notebook is formatted. Also see {0}", + "Plays the audio cue when a user explicitly formats a file.", + "Plays the audio cue whenever a file is formatted, including if it is set to format on save, type, or, paste, or run of a cell.", + "Never plays the audio cue." + ], + "vs/workbench/contrib/scrollLocking/browser/scrollLocking": [ + "Scrolling Locked", + "Lock Scrolling Enabled", + "Locked Scrolling", + "Synchronize Scrolling Editors", + "Locked Scrolling", + "Toggle Locked Scrolling Across Editors", + "Hold Locked Scrolling Across Editors" + ], "vs/workbench/contrib/share/browser/shareService": [ "The number of available share providers", "Choose how to share {0}" ], + "vs/workbench/browser/window": [ + "Are you sure you want to quit?", + "Are you sure you want to exit?", + "Are you sure you want to close the window?", + "&&Quit", + "&&Exit", + "&&Close Window", + "Do not ask me again", + "An unexpected error occurred that requires a reload of this page.", + "The workbench was unexpectedly disposed while running.", + "&&Reload", + "The browser interrupted the opening of a new tab or window. Press 'Open' to open it anyway.", + "&&Open", + "&&Learn More", + "&&Try Again", + "We launched {0} on your computer.\n\nIf {1} did not launch, try again or install it below.", + "&&Install", + "We launched {0} on your computer.\n\nIf {1} did not launch, try again below.", + "All done. You can close this tab now." + ], "vs/workbench/browser/parts/notifications/notificationsCenter": [ "No new notifications", "Notifications", "Notification Center Actions", + "Enable Do Not Disturb Mode", + "Disable Do Not Disturb Mode", + "More…", "Notifications Center" ], "vs/workbench/browser/parts/notifications/notificationsStatus": [ @@ -26599,24 +28998,26 @@ "{0} New Notifications ({1} in progress)", "Status Message" ], + "vs/workbench/browser/parts/notifications/notificationsAlerts": [ + "Error: {0}", + "Warning: {0}", + "Info: {0}" + ], "vs/workbench/browser/parts/notifications/notificationsCommands": [ + "Select sources to enable notifications for", "Notifications", "Show Notifications", "Hide Notifications", "Clear All Notifications", "Accept Notification Primary Action", "Toggle Do Not Disturb Mode", + "Toggle Do Not Disturb Mode By Source...", "Focus Notification Toast" ], "vs/workbench/browser/parts/notifications/notificationsToasts": [ "{0}, notification", "{0}, source: {1}, notification" ], - "vs/workbench/browser/parts/notifications/notificationsAlerts": [ - "Error: {0}", - "Warning: {0}", - "Info: {0}" - ], "vs/workbench/services/configuration/common/configurationEditing": [ "Error while writing to {0}. {1}", "Open Tasks Configuration", @@ -26661,16 +29062,259 @@ "Workspace Settings", "Folder Settings" ], - "vs/workbench/common/editor/textEditorModel": [ - "Language {0} was automatically detected and set as the language mode." + "vs/workbench/services/textfile/common/textFileEditorModelManager": [ + "Failed to save '{0}': {1}" + ], + "vs/workbench/common/editor/textEditorModel": [ + "Language {0} was automatically detected and set as the language mode." + ], + "vs/platform/theme/common/colors/chartsColors": [ + "The foreground color used in charts.", + "The color used for horizontal lines in charts.", + "The red color used in chart visualizations.", + "The blue color used in chart visualizations.", + "The yellow color used in chart visualizations.", + "The orange color used in chart visualizations.", + "The green color used in chart visualizations.", + "The purple color used in chart visualizations." + ], + "vs/platform/theme/common/colors/inputColors": [ + "Input box background.", + "Input box foreground.", + "Input box border.", + "Border color of activated options in input fields.", + "Background color of activated options in input fields.", + "Background hover color of options in input fields.", + "Foreground color of activated options in input fields.", + "Input box foreground color for placeholder text.", + "Input validation background color for information severity.", + "Input validation foreground color for information severity.", + "Input validation border color for information severity.", + "Input validation background color for warning severity.", + "Input validation foreground color for warning severity.", + "Input validation border color for warning severity.", + "Input validation background color for error severity.", + "Input validation foreground color for error severity.", + "Input validation border color for error severity.", + "Dropdown background.", + "Dropdown list background.", + "Dropdown foreground.", + "Dropdown border.", + "Button foreground color.", + "Button separator color.", + "Button background color.", + "Button background color when hovering.", + "Button border color.", + "Secondary button foreground color.", + "Secondary button background color.", + "Secondary button background color when hovering.", + "Background color of checkbox widget.", + "Background color of checkbox widget when the element it's in is selected.", + "Foreground color of checkbox widget.", + "Border color of checkbox widget.", + "Border color of checkbox widget when the element it's in is selected.", + "Keybinding label background color. The keybinding label is used to represent a keyboard shortcut.", + "Keybinding label foreground color. The keybinding label is used to represent a keyboard shortcut.", + "Keybinding label border color. The keybinding label is used to represent a keyboard shortcut.", + "Keybinding label border bottom color. The keybinding label is used to represent a keyboard shortcut." + ], + "vs/platform/theme/common/colors/editorColors": [ + "Editor background color.", + "Editor default foreground color.", + "Background color of sticky scroll in the editor", + "Background color of sticky scroll on hover in the editor", + "Border color of sticky scroll in the editor", + " Shadow color of sticky scroll in the editor", + "Background color of editor widgets, such as find/replace.", + "Foreground color of editor widgets, such as find/replace.", + "Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.", + "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.", + "Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.", + "Foreground color of error squigglies in the editor.", + "If set, color of double underlines for errors in the editor.", + "Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.", + "Foreground color of warning squigglies in the editor.", + "If set, color of double underlines for warnings in the editor.", + "Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.", + "Foreground color of info squigglies in the editor.", + "If set, color of double underlines for infos in the editor.", + "Foreground color of hint squigglies in the editor.", + "If set, color of double underlines for hints in the editor.", + "Color of active links.", + "Color of the editor selection.", + "Color of the selected text for high contrast.", + "Color of the selection in an inactive editor. The color must not be opaque so as not to hide underlying decorations.", + "Color for regions with the same content as the selection. The color must not be opaque so as not to hide underlying decorations.", + "Border color for regions with the same content as the selection.", + "Color of the current search match.", + "Color of the other search matches. The color must not be opaque so as not to hide underlying decorations.", + "Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.", + "Border color of the current search match.", + "Border color of the other search matches.", + "Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.", + "Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.", + "Background color of the editor hover.", + "Foreground color of the editor hover.", + "Border color of the editor hover.", + "Background color of the editor hover status bar.", + "Foreground color of inline hints", + "Background color of inline hints", + "Foreground color of inline hints for types", + "Background color of inline hints for types", + "Foreground color of inline hints for parameters", + "Background color of inline hints for parameters", + "The color used for the lightbulb actions icon.", + "The color used for the lightbulb auto fix actions icon.", + "The color used for the lightbulb AI icon.", + "Highlight background color of a snippet tabstop.", + "Highlight border color of a snippet tabstop.", + "Highlight background color of the final tabstop of a snippet.", + "Highlight border color of the final tabstop of a snippet.", + "Background color for text that got inserted. The color must not be opaque so as not to hide underlying decorations.", + "Background color for text that got removed. The color must not be opaque so as not to hide underlying decorations.", + "Background color for lines that got inserted. The color must not be opaque so as not to hide underlying decorations.", + "Background color for lines that got removed. The color must not be opaque so as not to hide underlying decorations.", + "Background color for the margin where lines got inserted.", + "Background color for the margin where lines got removed.", + "Diff overview ruler foreground for inserted content.", + "Diff overview ruler foreground for removed content.", + "Outline color for the text that got inserted.", + "Outline color for text that got removed.", + "Border color between the two text editors.", + "Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views.", + "The background color of unchanged blocks in the diff editor.", + "The foreground color of unchanged blocks in the diff editor.", + "The background color of unchanged code in the diff editor.", + "Shadow color of widgets such as find/replace inside the editor.", + "Border color of widgets such as find/replace inside the editor.", + "Toolbar background when hovering over actions using the mouse", + "Toolbar outline when hovering over actions using the mouse", + "Toolbar background when holding the mouse over actions", + "Color of focused breadcrumb items.", + "Background color of breadcrumb items.", + "Color of focused breadcrumb items.", + "Color of selected breadcrumb items.", + "Background color of breadcrumb item picker.", + "Current header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", + "Current content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", + "Incoming header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", + "Incoming content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", + "Common ancestor header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", + "Common ancestor content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.", + "Border color on headers and the splitter in inline merge-conflicts.", + "Current overview ruler foreground for inline merge-conflicts.", + "Incoming overview ruler foreground for inline merge-conflicts.", + "Common ancestor overview ruler foreground for inline merge-conflicts.", + "Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.", + "Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.", + "The color used for the problems error icon.", + "The color used for the problems warning icon.", + "The color used for the problems info icon." + ], + "vs/platform/theme/common/colors/minimapColors": [ + "Minimap marker color for find matches.", + "Minimap marker color for repeating editor selections.", + "Minimap marker color for the editor selection.", + "Minimap marker color for infos.", + "Minimap marker color for warnings.", + "Minimap marker color for errors.", + "Minimap background color.", + "Opacity of foreground elements rendered in the minimap. For example, \"#000000c0\" will render the elements with 75% opacity.", + "Minimap slider background color.", + "Minimap slider background color when hovering.", + "Minimap slider background color when clicked on." + ], + "vs/platform/theme/common/colors/listColors": [ + "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree outline color for the focused item when the list/tree is active and selected. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.", + "List/Tree background when hovering over items using the mouse.", + "List/Tree foreground when hovering over items using the mouse.", + "List/Tree drag and drop background when moving items over other items when using the mouse.", + "List/Tree drag and drop border color when moving items between items when using the mouse.", + "List/Tree foreground color of the match highlights when searching inside the list/tree.", + "List/Tree foreground color of the match highlights on actively focused items when searching inside the list/tree.", + "List/Tree foreground color for invalid items, for example an unresolved root in explorer.", + "Foreground color of list items containing errors.", + "Foreground color of list items containing warnings.", + "Background color of the type filter widget in lists and trees.", + "Outline color of the type filter widget in lists and trees.", + "Outline color of the type filter widget in lists and trees, when there are no matches.", + "Shadow color of the type filter widget in lists and trees.", + "Background color of the filtered match.", + "Border color of the filtered match.", + "List/Tree foreground color for items that are deemphasized.", + "Tree stroke color for the indentation guides.", + "Tree stroke color for the indentation guides that are not active.", + "Table border color between columns.", + "Background color for odd table rows." + ], + "vs/platform/theme/common/colors/miscColors": [ + "Border color of active sashes.", + "Badge background color. Badges are small information labels, e.g. for search results count.", + "Badge foreground color. Badges are small information labels, e.g. for search results count.", + "Scrollbar shadow to indicate that the view is scrolled.", + "Scrollbar slider background color.", + "Scrollbar slider background color when hovering.", + "Scrollbar slider background color when clicked on.", + "Background color of the progress bar that can show for long running operations." + ], + "vs/platform/theme/common/colors/menuColors": [ + "Border color of menus.", + "Foreground color of menu items.", + "Background color of menu items.", + "Foreground color of the selected menu item in menus.", + "Background color of the selected menu item in menus.", + "Border color of the selected menu item in menus.", + "Color of a separator menu item in menus." ], - "vs/workbench/services/textfile/common/textFileEditorModelManager": [ - "Failed to save '{0}': {1}" + "vs/platform/theme/common/colors/baseColors": [ + "Overall foreground color. This color is only used if not overridden by a component.", + "Overall foreground for disabled elements. This color is only used if not overridden by a component.", + "Overall foreground color for error messages. This color is only used if not overridden by a component.", + "Foreground color for description text providing additional information, for example for a label.", + "The default color for icons in the workbench.", + "Overall border color for focused elements. This color is only used if not overridden by a component.", + "An extra border around elements to separate them from others for greater contrast.", + "An extra border around active elements to separate them from others for greater contrast.", + "The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor.", + "Foreground color for links in text.", + "Foreground color for links in text when clicked on and on mouse hover.", + "Color for text separators.", + "Foreground color for preformatted text segments.", + "Background color for preformatted text segments.", + "Background color for block quotes in text.", + "Border color for block quotes in text.", + "Background color for code blocks in text." + ], + "vs/platform/theme/common/colors/quickpickColors": [ + "Quick picker background color. The quick picker widget is the container for pickers like the command palette.", + "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.", + "Quick picker title background color. The quick picker widget is the container for pickers like the command palette.", + "Quick picker color for grouping labels.", + "Quick picker color for grouping borders.", + "Please use quickInputList.focusBackground instead", + "Quick picker foreground color for the focused item.", + "Quick picker icon foreground color for the focused item.", + "Quick picker background color for the focused item." + ], + "vs/platform/theme/common/colors/searchColors": [ + "Color of the text in the search viewlet's completion message.", + "Color of the Search Editor query matches.", + "Border color of the Search Editor query matches." ], "vs/workbench/browser/parts/titlebar/titlebarPart": [ - "Focus Title Bar", - "Command Center", - "Layout Controls" + "Title actions", + "Focus Title Bar" ], "vs/workbench/services/configurationResolver/common/variableResolver": [ "Variable {0} can not be resolved. Please open an editor.", @@ -26692,25 +29336,23 @@ "vs/workbench/services/workingCopy/common/workingCopyHistoryTracker": [ "Undo / Redo" ], - "vs/workbench/services/extensions/common/extensionsUtil": [ - "Overwriting extension {0} with {1}.", - "Overwriting extension {0} with {1}.", - "Loading development extension at {0}" - ], "vs/workbench/services/extensions/common/extensionHostManager": [ "Measure Extension Host Latency" ], - "vs/workbench/contrib/extensions/common/reportExtensionIssueAction": [ - "Report Issue" - ], "vs/workbench/contrib/localization/common/localizationsActions": [ - "Configure Display Language", "Select Display Language", "Installed", "Available", "More Info", + "Configure Display Language", + "Changes the locale of VS Code based on installed language packs. Common languages include French, Chinese, Spanish, Japanese, German, Korean, and more.", "Clear Display Language Preference" ], + "vs/workbench/services/extensions/common/extensionsUtil": [ + "Overwriting extension {0} with {1}.", + "Overwriting extension {0} with {1}.", + "Loading development extension at {0}" + ], "vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions": [ "Performance Issue", "Report Issue", @@ -26720,33 +29362,61 @@ "Did you attach the CPU-Profile?", "This is a reminder to make sure that you have not forgotten to attach '{0}' to an existing performance issue." ], + "vs/workbench/contrib/extensions/common/reportExtensionIssueAction": [ + "Report Issue" + ], "vs/workbench/contrib/terminal/electron-sandbox/terminalRemote": [ "Create New Integrated Terminal (Local)" ], - "vs/workbench/contrib/tasks/common/taskTemplates": [ - "Executes .NET Core build command", - "Executes the build target", - "Example to run an arbitrary external command", - "Executes common maven commands" + "vs/workbench/contrib/terminal/browser/baseTerminalBackend": [ + "Pty Host Status", + "Pty Host", + "The connection to the terminal's pty host process is unresponsive, terminals may stop working. Click to manually restart the pty host.", + "Pty Host is unresponsive" ], - "vs/workbench/contrib/tasks/browser/taskQuickPick": [ - "Show All Tasks...", - "Configuration icon in the tasks selection list.", - "Icon for remove in the tasks selection list.", - "Configure Task", - "contributed", - "All {0} tasks", - "Remove Recently Used Task", - "recently used", - "configured", - "configured", - "Task detection for {0} tasks causes files in any workspace you open to be run as code. Enabling {0} task detection is a user setting and will apply to any workspace you open. \n\n Do you want to enable {0} task detection for all workspaces?", - "No", - "Select the task to run", - "$(gear) {0} task detection is turned off. Enable {1} task detection...", - "Go back ↩", - "No {0} tasks found. Go back ↩", - "There is no task provider registered for tasks of type \"{0}\"." + "vs/platform/theme/common/tokenClassificationRegistry": [ + "Colors and styles for the token.", + "Foreground color for the token.", + "Token background colors are currently not supported.", + "Sets the all font styles of the rule: 'italic', 'bold', 'underline' or 'strikethrough' or a combination. All styles that are not listed are unset. The empty string unsets all styles.", + "Font style must be 'italic', 'bold', 'underline' or 'strikethrough' or a combination. The empty string unsets all styles.", + "None (clear inherited style)", + "Sets or unsets the font style to bold. Note, the presence of 'fontStyle' overrides this setting.", + "Sets or unsets the font style to italic. Note, the presence of 'fontStyle' overrides this setting.", + "Sets or unsets the font style to underline. Note, the presence of 'fontStyle' overrides this setting.", + "Sets or unsets the font style to strikethrough. Note, the presence of 'fontStyle' overrides this setting.", + "Style for comments.", + "Style for strings.", + "Style for keywords.", + "Style for numbers.", + "Style for expressions.", + "Style for operators.", + "Style for namespaces.", + "Style for types.", + "Style for structs.", + "Style for classes.", + "Style for interfaces.", + "Style for enums.", + "Style for type parameters.", + "Style for functions", + "Style for member functions", + "Style for method (member functions)", + "Style for macros.", + "Style for variables.", + "Style for parameters.", + "Style for properties.", + "Style for enum members.", + "Style for events.", + "Style for decorators & annotations.", + "Style for labels. ", + "Style for all symbol declarations.", + "Style to use for references in documentation.", + "Style to use for symbols that are static.", + "Style to use for symbols that are abstract.", + "Style to use for symbols that are deprecated.", + "Style to use for write accesses.", + "Style to use for symbols that are async.", + "Style to use for symbols that are read-only." ], "vs/workbench/contrib/tasks/common/taskConfiguration": [ "Warning: options.cwd must be of type string. Ignoring value {0}\n", @@ -26778,16 +29448,38 @@ "Task has infos and is waiting...", "Beginning of detected errors for this run" ], - "vs/workbench/contrib/terminal/browser/baseTerminalBackend": [ - "Pty Host Status", - "Pty Host", - "The connection to the terminal's pty host process is unresponsive, terminals may stop working. Click to manually restart the pty host.", - "Pty Host is unresponsive" + "vs/workbench/contrib/tasks/common/taskTemplates": [ + "Executes .NET Core build command", + "Executes the build target", + "Example to run an arbitrary external command", + "Executes common maven commands" + ], + "vs/workbench/contrib/tasks/browser/taskQuickPick": [ + "Show All Tasks...", + "Configuration icon in the tasks selection list.", + "Icon for remove in the tasks selection list.", + "Configure Task", + "contributed", + "All {0} tasks", + "Remove Recently Used Task", + "recently used", + "configured", + "configured", + "Task detection for {0} tasks causes files in any workspace you open to be run as code. Enabling {0} task detection is a user setting and will apply to any workspace you open. \n\n Do you want to enable {0} task detection for all workspaces?", + "No", + "Select the task to run", + "$(gear) {0} task detection is turned off. Enable {1} task detection...", + "Go back ↩", + "No {0} tasks found. Go back ↩", + "There is no task provider registered for tasks of type \"{0}\"." ], "vs/workbench/contrib/localHistory/browser/localHistory": [ "Icon for a local history entry in the timeline view.", "Icon for restoring contents of a local history entry." ], + "vs/workbench/contrib/multiDiffEditor/browser/icons.contribution": [ + "Icon of the multi diff editor label." + ], "vs/workbench/contrib/debug/common/abstractDebugAdapter": [ "Timeout after {0} ms for '{1}'" ], @@ -26854,7 +29546,7 @@ ], "vs/platform/terminal/common/terminalPlatformConfiguration": [ "An optional set of arguments to run the shell executable with.", - "Controls whether or not the profile name overrides the auto detected one.", + "Whether or not to replace the dynamic terminal title that detects what program is running with the static profile name.", "A codicon ID to associate with the terminal icon.", "A theme color ID to associate with the terminal icon.", "An object with environment variables that will be added to the terminal profile process. Set to `null` to delete environment variables from the base environment.", @@ -26885,20 +29577,50 @@ "The default terminal profile on macOS.", "The default terminal profile on Windows." ], + "vs/base/browser/ui/findinput/findInput": [ + "input" + ], "vs/base/browser/ui/inputbox/inputBox": [ "Error: {0}", "Warning: {0}", "Info: {0}", - "for history", + " or {0} for history", + " ({0} for history)", "Cleared Input" ], - "vs/base/browser/ui/findinput/findInput": [ - "input" + "vs/editor/browser/widget/diffEditor/registrations.contribution": [ + "The border color for text that got moved in the diff editor.", + "The active border color for text that got moved in the diff editor.", + "The color of the shadow around unchanged region widgets.", + "Line decoration for inserts in the diff editor.", + "Line decoration for removals in the diff editor." ], - "vs/editor/contrib/codeAction/browser/lightBulbWidget": [ - "Show Code Actions. Preferred Quick Fix Available ({0})", - "Show Code Actions ({0})", - "Show Code Actions" + "vs/editor/browser/widget/diffEditor/commands": [ + "Toggle Collapse Unchanged Regions", + "Toggle Show Moved Code Blocks", + "Toggle Use Inline View When Space Is Limited", + "Diff Editor", + "Switch Side", + "Exit Compare Move", + "Collapse All Unchanged Regions", + "Show All Unchanged Regions", + "Revert", + "Accessible Diff Viewer", + "Go to Next Difference", + "Go to Previous Difference" + ], + "vs/editor/contrib/dropOrPasteInto/browser/copyPasteController": [ + "Whether the paste widget is showing", + "Show paste options...", + "No paste edits for '{0}' found", + "Running paste handlers. Click to cancel", + "Select Paste Action", + "Running paste handlers" + ], + "vs/editor/contrib/codeAction/browser/codeActionController": [ + "Context: {0} at line {1} and column {2}.", + "Hide Disabled", + "Show Disabled" ], "vs/editor/contrib/codeAction/browser/codeActionCommands": [ "Kind of the code action to run.", @@ -26930,35 +29652,24 @@ "Auto Fix...", "No auto fixes available" ], - "vs/editor/contrib/codeAction/browser/codeActionController": [ - "Context: {0} at line {1} and column {2}.", - "Hide Disabled", - "Show Disabled" + "vs/editor/contrib/codeAction/browser/lightBulbWidget": [ + "Run: {0}", + "Show Code Actions. Preferred Quick Fix Available ({0})", + "Show Code Actions ({0})", + "Show Code Actions" ], "vs/base/browser/ui/actionbar/actionViewItems": [ "{0} ({1})" ], - "vs/editor/contrib/dropOrPasteInto/browser/copyPasteController": [ - "Whether the paste widget is showing", - "Show paste options...", - "Running paste handlers. Click to cancel", - "Select Paste Action", - "Running paste handlers" - ], "vs/editor/contrib/dropOrPasteInto/browser/defaultProviders": [ - "Built-in", "Insert Plain Text", "Insert Uris", "Insert Uri", "Insert Paths", "Insert Path", "Insert Relative Paths", - "Insert Relative Path" - ], - "vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController": [ - "Whether the drop widget is showing", - "Show drop options...", - "Running drop handlers. Click to cancel" + "Insert Relative Path", + "Insert HTML" ], "vs/editor/contrib/find/browser/findWidget": [ "Icon for 'Find in Selection' in the editor find widget.", @@ -26989,22 +29700,20 @@ "{0} found for '{1}'", "Ctrl+Enter now inserts line break instead of replacing all. You can modify the keybinding for editor.action.replaceAll to override this behavior." ], + "vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController": [ + "Whether the drop widget is showing", + "Show drop options...", + "Running drop handlers. Click to cancel" + ], "vs/editor/contrib/folding/browser/foldingDecorations": [ "Background color behind folded ranges. The color must not be opaque so as not to hide underlying decorations.", "Color of the folding control in the editor gutter.", "Icon for expanded ranges in the editor glyph margin.", "Icon for collapsed ranges in the editor glyph margin.", "Icon for manually collapsed ranges in the editor glyph margin.", - "Icon for manually expanded ranges in the editor glyph margin." - ], - "vs/editor/contrib/format/browser/format": [ - "Made 1 formatting edit on line {0}", - "Made {0} formatting edits on line {1}", - "Made 1 formatting edit between lines {0} and {1}", - "Made {0} formatting edits between lines {1} and {2}" - ], - "vs/editor/contrib/inlineCompletions/browser/hoverParticipant": [ - "Suggestion:" + "Icon for manually expanded ranges in the editor glyph margin.", + "Click to expand the range.", + "Click to collapse the range." ], "vs/editor/contrib/inlineCompletions/browser/commands": [ "Show Next Inline Suggestion", @@ -27022,16 +29731,59 @@ "vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController": [ "Inspect this in the accessible view ({0})" ], + "vs/editor/contrib/inlineCompletions/browser/hoverParticipant": [ + "Suggestion:" + ], + "vs/editor/contrib/hover/browser/hoverActions": [ + "Show or Focus Hover", + "The hover will not automatically take focus.", + "The hover will take focus only if it is already visible.", + "The hover will automatically take focus when it appears.", + "Show Definition Preview Hover", + "Scroll Up Hover", + "Scroll Down Hover", + "Scroll Left Hover", + "Scroll Right Hover", + "Page Up Hover", + "Page Down Hover", + "Go To Top Hover", + "Go To Bottom Hover", + "Increase Hover Verbosity Level", + "Decrease Hover Verbosity Level", + "Show or focus the editor hover which shows documentation, references, and other content for a symbol at the current cursor position.", + "Show the definition preview hover in the editor.", + "Scroll up the editor hover.", + "Scroll down the editor hover.", + "Scroll left the editor hover.", + "Scroll right the editor hover.", + "Page up the editor hover.", + "Page down the editor hover.", + "Go to the top of the editor hover.", + "Go to the bottom of the editor hover." + ], + "vs/editor/contrib/hover/browser/markerHoverParticipant": [ + "View Problem", + "No quick fixes available", + "Checking for quick fixes...", + "No quick fixes available", + "Quick Fix..." + ], + "vs/editor/contrib/hover/browser/markdownHoverParticipant": [ + "Icon for increaseing hover verbosity.", + "Icon for decreasing hover verbosity.", + "Loading...", + "Rendering paused for long line for performance reasons. This can be configured via `editor.stopRenderingLineAfter`.", + "Tokenization is skipped for long lines for performance reasons. This can be configured via `editor.maxTokenizationLineLength`.", + "Increase Verbosity ({0})", + "Increase Verbosity", + "Decrease Verbosity ({0})", + "Decrease Verbosity" + ], "vs/editor/contrib/gotoSymbol/browser/peek/referencesController": [ "Whether reference peek is visible, like 'Peek References' or 'Peek Definition'", "Loading...", "{0} ({1})" ], - "vs/editor/contrib/gotoSymbol/browser/symbolNavigation": [ - "Whether there are symbol locations that can be navigated via keyboard-only.", - "Symbol {0} of {1}, {2} for next", - "Symbol {0} of {1}" - ], "vs/editor/contrib/gotoSymbol/browser/referencesModel": [ "in {0} on line {1} at column {2}", "{0} in {1} on line {2} at column {3}", @@ -27042,6 +29794,11 @@ "Found {0} symbols in {1}", "Found {0} symbols in {1} files" ], + "vs/editor/contrib/gotoSymbol/browser/symbolNavigation": [ + "Whether there are symbol locations that can be navigated via keyboard-only.", + "Symbol {0} of {1}, {2} for next", + "Symbol {0} of {1}" + ], "vs/editor/contrib/message/browser/messageController": [ "Whether the editor is currently showing an inline message" ], @@ -27071,25 +29828,6 @@ "Go to Definition ({0})", "Execute Command" ], - "vs/editor/contrib/hover/browser/markdownHoverParticipant": [ - "Loading...", - "Rendering paused for long line for performance reasons. This can be configured via `editor.stopRenderingLineAfter`.", - "Tokenization is skipped for long lines for performance reasons. This can be configured via `editor.maxTokenizationLineLength`." - ], - "vs/editor/contrib/hover/browser/markerHoverParticipant": [ - "View Problem", - "No quick fixes available", - "Checking for quick fixes...", - "No quick fixes available", - "Quick Fix..." - ], - "vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget": [ - "Icon for show next parameter hint.", - "Icon for show previous parameter hint.", - "{0} ({1})", - "Previous", - "Next" - ], "vs/editor/contrib/wordHighlighter/browser/highlightDecorations": [ "Background color of a symbol during read-access, like reading a variable. The color must not be opaque so as not to hide underlying decorations.", "Background color of a symbol during write-access, like writing to a variable. The color must not be opaque so as not to hide underlying decorations.", @@ -27107,21 +29845,26 @@ "{0}, hint", "Foreground color of the active item in the parameter hint." ], - "vs/editor/contrib/rename/browser/renameInputField": [ + "vs/editor/contrib/rename/browser/renameWidget": [ "Whether the rename input widget is visible", + "Whether the rename input widget is focused", + "{0} to Rename, {1} to Preview", + "Received {0} rename suggestions", "Rename input. Type new name and press Enter to commit.", - "{0} to Rename, {1} to Preview" + "Generate new name suggestions", + "Cancel" ], "vs/editor/contrib/stickyScroll/browser/stickyScrollActions": [ - "Toggle Sticky Scroll", - "&&Toggle Sticky Scroll", + "&&Toggle Editor Sticky Scroll", "Sticky Scroll", "&&Sticky Scroll", - "Focus Sticky Scroll", "&&Focus Sticky Scroll", - "Select next sticky scroll line", - "Select previous sticky scroll line", - "Go to focused sticky scroll line", + "Toggle Editor Sticky Scroll", + "Toggle/enable the editor sticky scroll which shows the nested scopes at the top of the viewport", + "Focus on the editor sticky scroll", + "Select the next editor sticky scroll line", + "Select the previous sticky scroll line", + "Go to the focused sticky scroll line", "Select Editor" ], "vs/editor/contrib/suggest/browser/suggestWidget": [ @@ -27142,49 +29885,25 @@ "{0}, {1}", "{0}, docs: {1}" ], - "vs/platform/theme/common/tokenClassificationRegistry": [ - "Colors and styles for the token.", - "Foreground color for the token.", - "Token background colors are currently not supported.", - "Sets the all font styles of the rule: 'italic', 'bold', 'underline' or 'strikethrough' or a combination. All styles that are not listed are unset. The empty string unsets all styles.", - "Font style must be 'italic', 'bold', 'underline' or 'strikethrough' or a combination. The empty string unsets all styles.", - "None (clear inherited style)", - "Sets or unsets the font style to bold. Note, the presence of 'fontStyle' overrides this setting.", - "Sets or unsets the font style to italic. Note, the presence of 'fontStyle' overrides this setting.", - "Sets or unsets the font style to underline. Note, the presence of 'fontStyle' overrides this setting.", - "Sets or unsets the font style to strikethrough. Note, the presence of 'fontStyle' overrides this setting.", - "Style for comments.", - "Style for strings.", - "Style for keywords.", - "Style for numbers.", - "Style for expressions.", - "Style for operators.", - "Style for namespaces.", - "Style for types.", - "Style for structs.", - "Style for classes.", - "Style for interfaces.", - "Style for enums.", - "Style for type parameters.", - "Style for functions", - "Style for member functions", - "Style for method (member functions)", - "Style for macros.", - "Style for variables.", - "Style for parameters.", - "Style for properties.", - "Style for enum members.", - "Style for events.", - "Style for decorators & annotations.", - "Style for labels. ", - "Style for all symbol declarations.", - "Style to use for references in documentation.", - "Style to use for symbols that are static.", - "Style to use for symbols that are abstract.", - "Style to use for symbols that are deprecated.", - "Style to use for write accesses.", - "Style to use for symbols that are async.", - "Style to use for symbols that are read-only." + "vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature": [ + "Fold Unchanged Region", + "Click or drag to show more above", + "Show Unchanged Region", + "Click or drag to show more below", + "{0} hidden lines", + "Double click to unfold" + ], + "vs/workbench/contrib/chat/browser/chatInputPart": [ + "Chat Input, Type to ask questions or type / for topics, press enter to send out the request. Use {0} for Chat Accessibility Help.", + "Chat Input, Type code here and press Enter to run. Use the Chat Accessibility Help command for more information.", + "Chat Input", + "More...", + "Use", + "Send to @{0}" + ], + "vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables": [ + "All Files", + "Search for relevant files in the workspace and provide context from them" ], "vs/workbench/api/browser/mainThreadWebviews": [ "An error occurred while loading view: {0}" @@ -27202,43 +29921,15 @@ "Custom editor '{0}' could not be saved.", "Edit" ], - "vs/workbench/contrib/comments/browser/commentsView": [ - "Filter (e.g. text, author)", - "Filter comments", - "{0} Unresolved Comments", - "Showing {0} of {1}", - "Comments for current workspace", - "Comments in {0}, full path {1}", - "Comment from ${0} at line {1} column {2} in {3}, source: {4}", - "Comment from ${0} in {1}, source: {2}", - "Collapse All", - "Expand All" - ], - "vs/workbench/contrib/comments/browser/commentsTreeViewer": [ - "Comments", - "{0} comments", - "1 comment", - "Image: {0}", - "Image", - "[Ln {0}]", - "[Ln {0}-{1}]", - "Last reply from {0}" - ], "vs/workbench/contrib/testing/common/testResult": [ "Test run at {0}" ], - "vs/workbench/browser/parts/compositeBarActions": [ - "{0} ({1})", - "{0} - {1}", - "Additional Views", - "{0} ({1})", - "Manage Extension", - "Hide '{0}'", - "Keep '{0}'", - "Hide Badge", - "Show Badge", - "Toggle View Pinned", - "Toggle View Badge" + "vs/base/browser/ui/tree/treeDefaults": [ + "Collapse All" + ], + "vs/workbench/browser/parts/views/checkbox": [ + "Checked", + "Unchecked" ], "vs/base/browser/ui/splitview/paneview": [ "{0} Section" @@ -27270,7 +29961,6 @@ "Unknown", "Private", "Whether the Ports view has focus.", - "Ports", "Tunnel View", "Set Port Label", "Port label", @@ -27278,13 +29968,11 @@ "Port number must be ≥ 0 and < {0}.", "May Require Sudo", "Port is already forwarded", - "Forward a Port", "Forward Port", "Port number or address (eg. 3000 or 10.10.10.10:2000).", "Unable to forward {0}:{1}. The host may not be available or that remote port may already be forwarded", "Unable to forward {0}:{1}. {2}", "No ports currently forwarded. Try running the {0} command", - "Stop Forwarding Port", "Choose a port to stop forwarding", "Open in Browser", "Preview in Editor", @@ -27303,7 +29991,10 @@ "HTTPS", "Port Visibility", "Change Port Protocol", - "The color of the icon for a port that has an associated running process." + "The color of the icon for a port that has an associated running process.", + "Ports", + "Forward a Port", + "Stop Forwarding Port" ], "vs/workbench/contrib/remote/browser/remoteIcons": [ "Getting started icon in the remote explorer view.", @@ -27324,13 +30015,6 @@ "Icon for forwarded ports that don't have a running process.", "Icon for forwarded ports that do have a running process." ], - "vs/base/browser/ui/tree/treeDefaults": [ - "Collapse All" - ], - "vs/workbench/browser/parts/views/checkbox": [ - "Checked", - "Unchecked" - ], "vs/workbench/browser/parts/editor/textCodeEditor": [ "Text Editor" ], @@ -27339,72 +30023,48 @@ "The file is not displayed in the text editor because it is either binary or uses an unsupported text encoding.", "Open Anyway" ], - "vs/workbench/browser/parts/activitybar/activitybarActions": [ - "Loading...", - "{0} is currently unavailable", - "Manage Trusted Extensions", - "Sign Out", - "You are not signed in to any accounts", - "Hide Accounts", - "Manage {0} (Profile)", + "vs/workbench/browser/parts/activitybar/activitybarPart": [ + "Menu", + "Hide Menu", + "Activity Bar Position", + "&&Default", + "Default", + "&&Top", + "Top", + "&&Bottom", + "Bottom", + "&&Hidden", + "Hidden", + "Activity Bar Position", + "Activity Bar Position", + "Activity Bar Position", + "Move Activity Bar to Side", + "Move Activity Bar to Top", + "Move Activity Bar to Bottom", + "Hide Activity Bar", "Previous Primary Side Bar View", "Next Primary Side Bar View", "Focus Activity Bar" ], - "vs/workbench/browser/parts/compositeBar": [ - "Active View Switcher" - ], - "vs/workbench/browser/parts/titlebar/menubarControl": [ - "&&File", - "&&Edit", - "&&Selection", - "&&View", - "&&Go", - "&&Terminal", - "&&Help", - "Preferences", - "Accessibility support is enabled for you. For the most accessible experience, we recommend the custom title bar style.", - "Open Settings", - "Focus Application Menu", - "Check for &&Updates...", - "Checking for Updates...", - "D&&ownload Update", - "Downloading Update...", - "Install &&Update...", - "Installing Update...", - "Restart to &&Update" - ], - "vs/workbench/browser/parts/compositePart": [ - "{0} actions", - "Views and More Actions...", - "{0} ({1})" + "vs/workbench/browser/parts/paneCompositePart": [ + "Drag a view here to display.", + "More Actions...", + "Views" ], "vs/workbench/browser/parts/sidebar/sidebarActions": [ "Focus into Primary Side Bar" ], - "vs/base/browser/ui/toolbar/toolbar": [ - "More Actions..." - ], - "vs/workbench/browser/parts/editor/editorPanes": [ - "Unable to open '{0}'", - "&&OK" - ], - "vs/workbench/browser/parts/editor/editorGroupWatermark": [ - "Show All Commands", - "Go to File", - "Open File", - "Open Folder", - "Open File or Folder", - "Open Recent", - "New Untitled Text File", - "Find in Files", - "Toggle Terminal", - "Start Debugging", - "Toggle Full Screen", - "Show Settings" + "vs/workbench/browser/parts/editor/editorGroupView": [ + "Empty editor group actions", + "{0} (empty)", + "{0}: Group {1}", + "Group {0}", + "{0}: Editor Group {1}", + "Editor Group {0}", + "Try saving or reverting the editor first and then try again." ], - "vs/base/browser/ui/iconLabel/iconLabelHover": [ - "Loading..." + "vs/workbench/browser/parts/editor/editorDropTarget": [ + "Hold __{0}__ to drop into editor" ], "vs/workbench/services/preferences/browser/keybindingsEditorModel": [ "System", @@ -27421,6 +30081,7 @@ "The enum options should be strings, but there is a non-string option. Please file an issue with the extension author.", "Incorrect type. Expected \"string\".", "Setting has an invalid type, expected {0}. Fix in JSON.", + "Error parsing the following regex both with and without the u flag:", "Value must be {0} or fewer characters long.", "Value must be {0} or more characters long.", "Value must match regex `{0}`.", @@ -27451,6 +30112,21 @@ "vs/base/browser/ui/selectBox/selectBoxCustom": [ "Select Box" ], + "vs/base/browser/ui/icons/iconSelectBox": [ + "Search icons", + "No results" + ], + "vs/platform/quickinput/browser/quickInput": [ + "Back", + "Press 'Enter' to confirm your input or 'Escape' to cancel", + "{0}/{1}", + "Type to narrow down results.", + "{0} (Press 'Enter' to confirm or 'Escape' to cancel)" + ], + "vs/base/browser/ui/hover/hoverWidget": [ + "Inspect this in the accessible view with {0}.", + "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding." + ], "vs/platform/quickinput/browser/quickInputController": [ "Toggle all checkboxes", "{0} Results", @@ -27460,10 +30136,6 @@ "Back ({0})", "Back" ], - "vs/base/browser/ui/hover/hoverWidget": [ - "Inspect this in the accessible view with {0}.", - "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding." - ], "vs/workbench/services/textMate/common/TMGrammars": [ "Contributes textmate tokenizers.", "Language identifier for which this syntax is contributed to.", @@ -27478,28 +30150,17 @@ "vs/base/browser/ui/keybindingLabel/keybindingLabel": [ "Unbound" ], - "vs/workbench/contrib/preferences/common/settingsEditorColorRegistry": [ - "The foreground color for a section header or active title.", - "The foreground color for a section header or hovered title.", - "The color of the modified setting indicator.", - "The color of the header container border.", - "The color of the Settings editor splitview sash border.", - "Settings editor dropdown background.", - "Settings editor dropdown foreground.", - "Settings editor dropdown border.", - "Settings editor dropdown list border. This surrounds the options and separates the options from the description.", - "Settings editor checkbox background.", - "Settings editor checkbox foreground.", - "Settings editor checkbox border.", - "Settings editor text input box background.", - "Settings editor text input box foreground.", - "Settings editor text input box border.", - "Settings editor number input box background.", - "Settings editor number input box foreground.", - "Settings editor number input box border.", - "The background color of a settings row when focused.", - "The background color of a settings row when hovered.", - "The color of the row's top and bottom border when the row is focused." + "vs/workbench/contrib/preferences/browser/preferencesWidgets": [ + "User", + "Remote", + "Workspace", + "Folder", + "Settings Switcher", + "User", + "Remote", + "Workspace", + "User", + "Workspace" ], "vs/workbench/contrib/preferences/browser/preferencesRenderers": [ "Edit", @@ -27519,17 +30180,31 @@ "Manage Workspace Trust", "Unsupported Property" ], - "vs/workbench/contrib/preferences/browser/preferencesWidgets": [ - "User", - "Remote", - "Workspace", - "Folder", - "Settings Switcher", - "User", - "Remote", - "Workspace", - "User", - "Workspace" + "vs/base/browser/ui/toolbar/toolbar": [ + "More Actions..." + ], + "vs/workbench/contrib/preferences/common/settingsEditorColorRegistry": [ + "The foreground color for a section header or active title.", + "The foreground color for a section header or hovered title.", + "The color of the modified setting indicator.", + "The color of the header container border.", + "The color of the Settings editor splitview sash border.", + "Settings editor dropdown background.", + "Settings editor dropdown foreground.", + "Settings editor dropdown border.", + "Settings editor dropdown list border. This surrounds the options and separates the options from the description.", + "Settings editor checkbox background.", + "Settings editor checkbox foreground.", + "Settings editor checkbox border.", + "Settings editor text input box background.", + "Settings editor text input box foreground.", + "Settings editor text input box border.", + "Settings editor number input box background.", + "Settings editor number input box foreground.", + "Settings editor number input box border.", + "The background color of a settings row when focused.", + "The background color of a settings row when hovered.", + "The color of the row's top and bottom border when the row is focused." ], "vs/workbench/contrib/preferences/browser/settingsLayout": [ "Commonly Used", @@ -27539,6 +30214,7 @@ "Font", "Formatting", "Diff Editor", + "Multi-File Diff Editor", "Minimap", "Suggestions", "Files", @@ -27552,6 +30228,7 @@ "Window", "New Window", "Features", + "Accessibility Signals", "Accessibility", "Explorer", "Search", @@ -27567,7 +30244,6 @@ "Remote", "Timeline", "Notebook", - "Audio Cues", "Merge Editor", "Chat", "Application", @@ -27581,6 +30257,26 @@ "Security", "Workspace" ], + "vs/workbench/contrib/preferences/browser/tocTree": [ + "Settings Table of Contents", + "{0}, group" + ], + "vs/workbench/contrib/preferences/browser/settingsSearchMenu": [ + "Modified", + "Add or remove modified settings filter", + "Extension ID...", + "Add extension ID filter", + "Feature...", + "Add feature filter", + "Tag...", + "Add tag filter", + "Language...", + "Add language ID filter", + "Online services", + "Show settings for online services", + "Policy services", + "Show settings for policy services" + ], "vs/workbench/contrib/preferences/browser/settingsTree": [ "Extensions", "The setting has been configured in the current scope.", @@ -27601,29 +30297,103 @@ "Sync This Setting", "Apply Setting to all Profiles" ], - "vs/workbench/contrib/preferences/browser/tocTree": [ - "Settings Table of Contents", - "{0}, group" + "vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp": [ + "The chat view is comprised of an input box and a request/response list. The input box is used to make requests and the list is used to display responses.", + "In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.", + "In the input box, inspect the last response in the accessible view {0}", + "With the input box focused, inspect the last response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.", + "In the input box, navigate to the suggested follow up question (Shift+Tab) and press Enter to run it.", + "Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.", + "To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command ({0}).", + "To focus the chat request/response list, which can be navigated with up and down arrows, invoke The Focus Chat List command, which is currently not triggerable by a keybinding.", + "To focus the input box for chat requests, invoke the Focus Chat Input command ({0}).", + "To focus the input box for chat requests, invoke the Focus Chat Input command, which is currently not triggerable by a keybinding.", + "To focus the next code block within a response, invoke the Chat: Next Code Block command ({0}).", + "To focus the next code block within a response, invoke the Chat: Next Code Block command, which is currently not triggerable by a keybinding.", + "To focus the next file tree within a response, invoke the Chat: Next File Tree command ({0}).", + "To focus the next file tree within a response, invoke the Chat: Next File Tree command, which is currently not triggerable by a keybinding.", + "To clear the request/response list, invoke the Chat Clear command ({0}).", + "To clear the request/response list, invoke the Chat Clear command, which is currently not triggerable by a keybinding.", + "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect.", + "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat ({0}).", + "In the input box, use {0} and {1} to navigate your request history. Edit input and use enter or the submit button to run a new request.", + "In the input box, inspect the response in the accessible view {0}.", + "With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.", + "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands.", + "If a fix action is invoked, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing.", + "Once in the diff editor, enter review mode with ({0}). Use up and down arrows to navigate lines with the proposed changes.", + "Tab again to enter the Diff editor with the changes and enter review mode with the Go to Next Difference Command. Use Up/DownArrow to navigate lines with the proposed changes.", + "Use tab to reach conditional parts like commands, status, message responses and more.", + "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring." ], - "vs/workbench/contrib/preferences/browser/settingsSearchMenu": [ + "vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys": [ + "Whether an inline suggestion is visible", + "Whether the inline suggestion starts with whitespace", + "Whether the inline suggestion starts with whitespace that is less than what would be inserted by tab", + "Whether suggestions should be suppressed for the current suggestion" + ], + "vs/workbench/contrib/chat/browser/codeBlockPart": [ + "Code block", + "Toolbar for code block which can be reached via tab", + "Code block toolbar", + "Code block {0}", + "{0} vulnerabilities", + "{0} vulnerability", + "Code block", + "Original", "Modified", - "Add or remove modified settings filter", - "Extension ID...", - "Add extension ID filter", - "Feature...", - "Add feature filter", - "Tag...", - "Add tag filter", - "Language...", - "Add language ID filter", - "Online services", - "Show settings for online services", - "Policy services", - "Show settings for policy services" + "Toolbar for code block which can be reached via tab", + "Code block toolbar", + "Code Edits", + "Made {0} changes in [[{1}]]", + "Made 1 change in [[{0}]]", + "The original file has been modified.", + "Do you want to apply the changes anyway?" + ], + "vs/workbench/contrib/notebook/browser/controller/cellOperations": [ + "Cannot join cells of different kinds", + "Join Notebook Cells" + ], + "vs/workbench/contrib/chat/browser/chatAccessibilityProvider": [ + "Chat", + "1 file tree", + "{0} file trees", + "{0} {1} {2}", + "{0} {1}", + "{0} 1 code block: {1} {2}", + "{0} 1 code block: {1}", + "{0} {1} code blocks: {2}", + "{0} {1} code blocks" + ], + "vs/workbench/contrib/chat/browser/chatListRenderer": [ + "used {0}", + "using {0}", + "used {0}", + "using {0}", + "Used {0} references", + "Used {0} reference", + "{0}, expanded", + "{0}, collapsed", + "Button not available in restored chat", + "Making changes...", + "Made changes.", + "File Tree", + "Used References" + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatStrategies": [ + "Nothing changed.", + "$(info) Accept or Discard 1 change.", + "1 change", + "$(info) Accept or Discard {0} changes.", + "{0} changes", + "Review (accept or discard) all changes before continuing." + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget": [ + "Closed inline chat widget" ], "vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView": [ - "Select Notebook Kernel", - "Notebook Kernel Args" + "Notebook Kernel Args", + "Select Notebook Kernel" ], "vs/workbench/contrib/notebook/browser/notebookExtensionPoint": [ "Contributes notebook document provider.", @@ -27652,12 +30422,21 @@ "Contributes notebook preloads.", "Type of the notebook.", "Path to file loaded in the webview.", - "Paths to additional resources that should be allowed in the webview." + "Paths to additional resources that should be allowed in the webview.", + "ID", + "Name", + "Name", + "Mimetypes", + "Notebooks", + "Notebook Renderers" + ], + "vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView": [ + "Empty markdown cell, double-click or press enter to edit.", + "No renderer found for '$0'", + "Could not render content for '$0'", + "Notebook webview content" ], "vs/workbench/contrib/notebook/browser/notebookEditorWidget": [ - "Notebook\nUse {0} for accessibility help", - "Notebook\nRun the Open Accessibility Help command for more information", - "Notebook", "The border color for notebook cells.", "The color of the notebook cell editor border.", "The error icon color of notebook cells in the cell status bar.", @@ -27683,12 +30462,6 @@ "Cell editor background color.", "Notebook background color." ], - "vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView": [ - "Empty markdown cell, double-click or press enter to edit.", - "No renderer found for '$0'", - "Could not render content for '$0'", - "Notebook webview content" - ], "vs/workbench/services/workingCopy/common/fileWorkingCopyManager": [ "File Created", "File Replaced", @@ -27698,11 +30471,10 @@ "Deleted", "'{0}' already exists. Do you want to replace it?", "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", - "&&Replace" - ], - "vs/platform/actions/browser/toolbar": [ - "Hide", - "Reset Menu" + "&&Replace", + "'{0}' is marked as read-only. Do you want to save anyway?", + "Paths can be configured as read-only via settings.", + "&&Save Anyway" ], "vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy": [ "Currently Selected", @@ -27722,87 +30494,86 @@ "Detecting Kernels", "Select Kernel" ], - "vs/workbench/contrib/notebook/browser/controller/cellOperations": [ - "Cannot join cells of different kinds", - "Join Notebook Cells" + "vs/workbench/contrib/comments/common/commentContextKeys": [ + "Whether the position at the active cursor has a commenting range", + "Whether the active editor has a commenting range", + "Whether the open workspace has either comments or commenting ranges.", + "Set when the comment thread has no comments", + "Set when the comment has no input", + "The context value of the comment", + "The context value of the comment thread", + "The comment controller id associated with a comment thread", + "Set when the comment is focused" + ], + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView": [ + "Notebook Variables" + ], + "vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext": [ + "Whether the cell chat editor is focused", + "Whether the cell chat editor has an active request", + "Whether the user did changes ontop of the notebook cell chat", + "Whether the focus of the notebook editor is above or below the cell chat" + ], + "vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController": [ + "Ask a question", + "AI-generated code may be incorrect", + "Ask a question", + "AI-generated code may be incorrect" + ], + "vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions": [ + "Indent Using Tabs", + "Indent Using Spaces", + "Change Tab Display Size", + "Convert Indentation to Spaces", + "Convert Indentation to Tabs", + "Select Tab Size for Current File", + "Convert Indentation" ], "vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget": [ "{0} found", "{0} found for '{1}'", "{0} found for '{1}'" ], + "vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions": [ + "Cursor Up", + "Cursor Down", + "Focus Chat Widget", + "Focus Next Cell Chat Widget", + "Accept", + "Accept Changes", + "Discard", + "Helpful", + "Unhelpful", + "Report Issue", + "Generate", + "Start Chat to Generate Code", + "Start Chat to Generate Code", + "Generate", + "Start Chat to Generate Code", + "Generate", + "Start Chat to Generate Code", + "Focus Chat", + "Focus Next Cell", + "Focus Previous Cell", + "Make Request", + "Stop Request", + "Close Chat", + "Accept Changes", + "Previous From History", + "Next From History", + "Generate" + ], "vs/editor/contrib/codeAction/browser/codeAction": [ "An unknown error occurred while applying the code action" ], - "vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp": [ - "The chat view is comprised of an input box and a request/response list. The input box is used to make requests and the list is used to display responses.", - "In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.", - "In the input box, inspect the last response in the accessible view via {0}", - "With the input box focused, inspect the last response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.", - "Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.", - "To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command ({0}).", - "To focus the chat request/response list, which can be navigated with up and down arrows, invoke The Focus Chat List command, which is currently not triggerable by a keybinding.", - "To focus the input box for chat requests, invoke the Focus Chat Input command ({0})", - "To focus the input box for chat requests, invoke the Focus Chat Input command, which is currently not triggerable by a keybinding.", - "To focus the next code block within a response, invoke the Chat: Next Code Block command ({0}).", - "To focus the next code block within a response, invoke the Chat: Next Code Block command, which is currently not triggerable by a keybinding.", - "To focus the next file tree within a response, invoke the Chat: Next File Tree command ({0}).", - "To focus the next file tree within a response, invoke the Chat: Next File Tree command, which is currently not triggerable by a keybinding.", - "To clear the request/response list, invoke the Chat Clear command ({0}).", - "To clear the request/response list, invoke the Chat Clear command, which is currently not triggerable by a keybinding.", - "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect.", - "It can be activated via code actions or directly using the command: Inline Chat: Start Code Chat ({0}).", - "In the input box, use {0} and {1} to navigate your request history. Edit input and use enter or the submit button to run a new request.", - "In the input box, inspect the response in the accessible view via {0}", - "With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.", - "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands.", - "If a fix action is invoked, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing.", - "Once in the diff editor, enter review mode with ({0}). Use up and down arrows to navigate lines with the proposed changes.", - "Tab again to enter the Diff editor with the changes and enter review mode with the Go to Next Difference Command. Use Up/DownArrow to navigate lines with the proposed changes.", - "Use tab to reach conditional parts like commands, status, message responses and more.", - "Audio cues can be changed via settings with a prefix of audioCues.chat. By default, if a request takes more than 4 seconds, you will hear an audio cue indicating that progress is still occurring." - ], - "vs/workbench/contrib/chat/browser/chatInputPart": [ - "Chat Input, Type to ask questions or type / for topics, press enter to send out the request. Use {0} for Chat Accessibility Help.", - "Chat Input, Type code here and press Enter to run. Use the Chat Accessibility Help command for more information.", - "Chat Input" - ], - "vs/workbench/contrib/chat/browser/chatListRenderer": [ - "Chat", - "Command: {0}", - "Commands: {0}", - "1 file tree", - "{0} file trees", - "{0} {1} {2}", - "{0} {1}", - "{0} 1 code block: {1} {2}", - "{0} 1 code block: {1}", - "{0} {1} code blocks: {2}", - "{0} {1} code blocks", - "Code block", - "Toolbar for code block which can be reached via tab", - "Code block toolbar", - "Code block {0}", - "File Tree" - ], - "vs/workbench/contrib/inlineChat/browser/inlineChatStrategies": [ - "Nothing changed", - "Changed 1 line", - "Changed {0} lines" - ], - "vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys": [ - "Whether an inline suggestion is visible", - "Whether the inline suggestion starts with whitespace", - "Whether the inline suggestion starts with whitespace that is less than what would be inserted by tab", - "Whether suggestions should be suppressed for the current suggestion" - ], - "vs/workbench/contrib/inlineChat/browser/inlineChatWidget": [ - "Inline Chat Input", - "Original", - "Modified", - "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", - "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information.", - "Closed inline chat widget" + "vs/platform/quickinput/browser/commandsQuickAccess": [ + "recently used", + "similar commands", + "commonly used", + "other commands", + "similar commands", + "{0}, {1}", + "Command '{0}' resulted in an error" ], "vs/workbench/contrib/testing/browser/theme": [ "Color for the 'failed' icon in the test explorer.", @@ -27813,11 +30584,28 @@ "Color for the 'Unset' icon in the test explorer.", "Color for the 'Skipped' icon in the test explorer.", "Color of the peek view borders and arrow.", + "Color of the peek view borders and arrow when peeking a logged message.", "Color of the peek view borders and arrow.", + "Color of the peek view borders and arrow when peeking a logged message.", + "Background color of text that was covered.", + "Border color of text that was covered.", + "Gutter color of regions where code was covered.", + "Background of the widget shown for an uncovered branch.", + "Background color of text that was not covered.", + "Border color of text that was not covered.", + "Gutter color of regions where code not covered.", + "Background for the badge indicating execution count", + "Foreground for the badge indicating execution count", "Text color of test error messages shown inline in the editor.", "Margin color beside error messages shown inline in the editor.", "Text color of test info messages shown inline in the editor.", - "Margin color beside info messages shown inline in the editor." + "Margin color beside info messages shown inline in the editor.", + "Retired color for the 'Errored' icon in the test explorer.", + "Retired color for the 'failed' icon in the test explorer.", + "Retired color for the 'passed' icon in the test explorer.", + "Retired color for the 'Queued' icon in the test explorer.", + "Retired color for the 'Unset' icon in the test explorer.", + "Retired color for the 'Skipped' icon in the test explorer." ], "vs/workbench/contrib/testing/common/constants": [ "Errored", @@ -27845,11 +30633,7 @@ "Unhide All Tests" ], "vs/workbench/contrib/terminal/browser/xterm/xtermTerminal": [ - "The terminal has no selection to copy", - "Yes", - "No", - "Don't Show Again", - "Terminal GPU acceleration appears to be slow on your computer. Would you like to switch to disable it which may improve performance? [Read more about terminal settings](https://code.visualstudio.com/docs/editor/integrated-terminal#_changing-how-the-terminal-is-rendered)." + "The terminal has no selection to copy" ], "vs/workbench/contrib/terminal/common/terminalColorRegistry": [ "The background color of the terminal, this allows coloring the terminal differently to the panel.", @@ -27874,14 +30658,27 @@ "Border on the side of the terminal tab in the panel. This defaults to tab.activeBorder.", "'{0}' ANSI color in the terminal." ], - "vs/platform/quickinput/browser/commandsQuickAccess": [ - "recently used", - "similar commands", - "commonly used", - "other commands", - "similar commands", - "{0}, {1}", - "Command '{0}' resulted in an error" + "vs/workbench/contrib/files/browser/views/explorerDecorationsProvider": [ + "Unable to resolve workspace folder ({0})", + "Symbolic Link", + "Unknown File Type", + "Explorer" + ], + "vs/workbench/contrib/files/browser/views/explorerViewer": [ + "Files Explorer", + "Type file name. Press Enter to confirm or Escape to cancel.", + "Are you sure you want to change the order of multiple root folders in your workspace?", + "Are you sure you want to move the following {0} files into '{1}'?", + "Are you sure you want to change the order of root folder '{0}' in your workspace?", + "Are you sure you want to move '{0}' into '{1}'?", + "Do not ask me again", + "&&Move", + "Copy {0}", + "Copying {0}", + "Move {0}", + "Moving {0}", + "{0} folders", + "{0} files" ], "vs/workbench/contrib/files/browser/fileImportExport": [ "Uploading", @@ -27918,37 +30715,6 @@ "This action is irreversible!", "&&Replace" ], - "vs/workbench/contrib/files/browser/views/explorerViewer": [ - "Files Explorer", - "Type file name. Press Enter to confirm or Escape to cancel.", - "Are you sure you want to change the order of multiple root folders in your workspace?", - "Are you sure you want to move the following {0} files into '{1}'?", - "Are you sure you want to change the order of root folder '{0}' in your workspace?", - "Are you sure you want to move '{0}' into '{1}'?", - "Do not ask me again", - "&&Move", - "Copy {0}", - "Copying {0}", - "Move {0}", - "Moving {0}", - "{0} folders", - "{0} files" - ], - "vs/workbench/contrib/files/browser/views/explorerDecorationsProvider": [ - "Unable to resolve workspace folder ({0})", - "Symbolic Link", - "Unknown File Type", - "Explorer" - ], - "vs/workbench/contrib/searchEditor/browser/searchEditorSerialization": [ - "All backslashes in Query string must be escaped (\\\\)", - "{0} files", - "1 file", - "{0} results", - "1 result", - "No Results", - "The result set only contains a subset of all matches. Be more specific in your search to narrow down the results." - ], "vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree": [ "Bulk Edit", "Renaming {0} to {1}, also making text edits", @@ -27967,8 +30733,12 @@ "(deleting)", "{0} - {1}" ], - "vs/workbench/contrib/search/browser/searchFindInput": [ - "Notebook Find Filters" + "vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview": [ + "Other" + ], + "vs/workbench/contrib/search/browser/replaceService": [ + "Search and Replace", + "{0} ↔ {1} (Replace Preview)" ], "vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess": [ "To go to a symbol, first open a text editor with symbol information.", @@ -28005,20 +30775,35 @@ "fields ({0})", "constants ({0})" ], - "vs/workbench/contrib/search/browser/replaceService": [ - "Search and Replace", - "{0} ↔ {1} (Replace Preview)" + "vs/workbench/contrib/search/browser/searchFindInput": [ + "Use AI", + "Notebook Find Filters" + ], + "vs/workbench/contrib/searchEditor/browser/searchEditorSerialization": [ + "All backslashes in Query string must be escaped (\\\\)", + "{0} files", + "1 file", + "{0} results", + "1 result", + "No Results", + "The result set only contains a subset of all matches. Be more specific in your search to narrow down the results." + ], + "vs/workbench/contrib/scm/browser/dirtyDiffSwitcher": [ + "Switch quick diff base", + "Switch Quick Diff Base" + ], + "vs/workbench/contrib/scm/browser/menus": [ + "Share" ], "vs/workbench/contrib/debug/browser/baseDebugView": [ "Click to expand" ], - "vs/workbench/contrib/debug/browser/debugSessionPicker": [ - "Search debug sessions by name", - "Start a New Debug Session", - "Session {0} spawned from {1}" - ], - "vs/workbench/contrib/debug/common/loadedScriptsPicker": [ - "Search loaded scripts by name" + "vs/workbench/contrib/debug/browser/debugConfigurationManager": [ + "Edit Debug Configuration in launch.json", + "Select Launch Configuration", + "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", + "workspace", + "user settings" ], "vs/workbench/contrib/debug/browser/debugAdapterManager": [ "Debugger 'type' can not be omitted and must be of type 'string'.", @@ -28034,36 +30819,13 @@ "Install extension...", "Select debugger" ], - "vs/workbench/contrib/debug/browser/debugConfigurationManager": [ - "Edit Debug Configuration in launch.json", - "Select Launch Configuration", - "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", - "workspace", - "user settings" - ], - "vs/workbench/contrib/debug/browser/debugTaskRunner": [ - "Errors exist after running preLaunchTask '{0}'.", - "Error exists after running preLaunchTask '{0}'.", - "The preLaunchTask '{0}' terminated with exit code {1}.", - "The preLaunchTask '{0}' terminated.", - "&&Debug Anyway", - "&&Show Errors", - "Abort", - "Remember my choice in user settings", - "&&Debug Anyway", - "Remember my choice for this task", - "Task '{0}' can not be referenced from a launch configuration that is in a different workspace folder.", - "Could not find the task '{0}'.", - "Could not find the specified task.", - "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", - "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined." - ], "vs/workbench/contrib/debug/browser/debugSession": [ "No debugger available, can not send '{0}'", "No debugger available, can not send '{0}'", "No debugger available, can not send '{0}'", "No debugger available, can not send '{0}'", "No debugger available, can not send '{0}'", + "Session does not support breakpoints with bytes", "No debugger available, can not send '{0}'", "Session is not ready for breakpoints", "No debugger available, can not send '{0}'", @@ -28096,25 +30858,51 @@ "No debugger available, can not send '{0}'", "No debugger available, can not send '{0}'", "No debugger available, can not send '{0}'", + "Started running without debugging.", "Debugging started.", "Debugging stopped." ], + "vs/workbench/contrib/debug/browser/debugTaskRunner": [ + "Errors exist after running preLaunchTask '{0}'.", + "Error exists after running preLaunchTask '{0}'.", + "The preLaunchTask '{0}' terminated with exit code {1}.", + "The preLaunchTask '{0}' terminated.", + "&&Debug Anyway", + "&&Show Errors", + "Abort", + "Remember my choice in user settings", + "&&Debug Anyway", + "Remember my choice for this task", + "Task '{0}' can not be referenced from a launch configuration that is in a different workspace folder.", + "Could not find the task '{0}'.", + "Could not find the specified task.", + "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", + "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined." + ], "vs/workbench/contrib/debug/common/debugSource": [ "Unknown Source" ], - "vs/workbench/contrib/scm/browser/menus": [ - "Share" + "vs/workbench/contrib/debug/common/loadedScriptsPicker": [ + "Search loaded scripts by name" ], - "vs/workbench/contrib/scm/browser/dirtyDiffSwitcher": [ - "Switch quick diff base", - "Switch Quick Diff Base" + "vs/workbench/contrib/debug/browser/debugSessionPicker": [ + "Search debug sessions by name", + "Start a New Debug Session", + "Session {0} spawned from {1}" ], - "vs/base/browser/ui/findinput/replaceInput": [ - "input", - "Preserve Case" + "vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget": [ + "Icon for show next parameter hint.", + "Icon for show previous parameter hint.", + "{0} ({1})", + "Previous", + "Next" ], - "vs/base/browser/ui/dropdown/dropdownActionViewItem": [ - "More Actions..." + "vs/workbench/contrib/markers/browser/markersTreeViewer": [ + "Problems View", + "Icon indicating that multiple lines are shown in the markers view.", + "Icon indicating that multiple lines are collapsed in the markers view.", + "Show message in single line", + "Show message in multiple lines" ], "vs/workbench/contrib/markers/browser/markersTable": [ "Code", @@ -28122,6 +30910,39 @@ "File", "Source" ], + "vs/base/browser/ui/dropdown/dropdownActionViewItem": [ + "More Actions..." + ], + "vs/workbench/contrib/comments/browser/commentsModel": [ + "There are no comments in this workspace yet." + ], + "vs/workbench/contrib/comments/browser/commentColors": [ + "Icon color for resolved comments.", + "Icon color for unresolved comments.", + "Background color for comment reply input box.", + "Color of borders and arrow for resolved comments.", + "Color of borders and arrow for unresolved comments.", + "Color of background for comment ranges.", + "Color of background for currently selected or hovered comment range." + ], + "vs/workbench/contrib/comments/browser/commentsViewActions": [ + "Focus Comments view", + "Clear filter text", + "Focus comments filter", + "Show Unresolved", + "Comments", + "Show Unresolved", + "Show Resolved", + "Comments", + "Show Resolved" + ], + "vs/workbench/contrib/comments/browser/commentGlyphWidget": [ + "Editor gutter decoration color for commenting ranges. This color should be opaque.", + "Editor overview ruler decoration color for resolved comments. This color should be opaque.", + "Editor overview ruler decoration color for unresolved comments. This color should be opaque.", + "Editor gutter decoration color for commenting glyphs.", + "Editor gutter decoration color for commenting glyphs for unresolved comment threads." + ], "vs/workbench/contrib/mergeEditor/common/mergeEditor": [ "The editor is a merge editor", "The editor is a the result editor of a merge editor.", @@ -28132,13 +30953,6 @@ "The uri of the baser of a merge editor", "The uri of the result of a merge editor" ], - "vs/workbench/contrib/markers/browser/markersTreeViewer": [ - "Problems View", - "Icon indicating that multiple lines are shown in the markers view.", - "Icon indicating that multiple lines are collapsed in the markers view.", - "Show message in single line", - "Show message in multiple lines" - ], "vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel": [ "Do you want keep the merge result of {0} files?", "Do you want keep the merge result of {0}?", @@ -28168,7 +30982,7 @@ "The file contains unhandled conflicts.", "&&Close with Conflicts", "&&Close", - "Don't ask again" + "Do not ask me again" ], "vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView": [ "Base", @@ -28191,6 +31005,13 @@ "Undo accept", "Undo accept (currently second)" ], + "vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView": [ + "Result", + "{0} Conflict Remaining", + "{0} Conflicts Remaining ", + "Go to next conflict", + "All conflicts handled, the merge can be completed now." + ], "vs/workbench/contrib/mergeEditor/browser/view/colors": [ "The background color for changes.", "The background color for word changes.", @@ -28206,24 +31027,38 @@ "The background color of decorations in input 1.", "The background color of decorations in input 2." ], - "vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView": [ - "Result", - "{0} Conflict Remaining", - "{0} Conflicts Remaining ", - "Go to next conflict", - "All conflicts handled, the merge can be completed now." - ], - "vs/workbench/contrib/comments/browser/commentsController": [ - "Line {0}", - "Lines {0} to {1}", - "Editor has commenting ranges, run the command Open Accessibility Help ({0}), for more information.", - "Editor has commenting ranges, run the command Open Accessibility Help, which is currently not triggerable via keybinding, for more information.", - "Editor has commenting ranges.", - "Select Comment Provider" - ], "vs/workbench/contrib/customEditor/common/contributedCustomEditors": [ "Built-in" ], + "vs/platform/files/browser/htmlFileSystemProvider": [ + "Rename is only supported for files.", + "Insufficient permissions. Please retry and allow the operation." + ], + "vs/workbench/contrib/extensions/browser/extensionsViewer": [ + "Error", + "Unknown Extension:", + "Extensions" + ], + "vs/workbench/contrib/extensions/browser/extensionFeaturesTab": [ + "Activation", + "Uncaught Errors ({0})", + "Messages ({0})", + "Last Request: `{0}`", + "Requests (Session) : `{0}`", + "Requests (Overall): `{0}`", + "Runtime Status", + "No features contributed.", + "Extension Features", + "No Access", + "Enable '{0}' Feature", + "Would you like to revoke '{0}' extension to access '{1}' feature?", + "Would you like to allow '{0}' extension to access '{1}' feature?", + "Revoke Access", + "Allow Access", + "Cancel", + "Revoke Access", + "Allow Access" + ], "vs/workbench/contrib/extensions/browser/extensionsWidgets": [ "Average rating: {0} out of 5", "Sponsor", @@ -28231,8 +31066,9 @@ "This extension is ignored during sync.", "Activation time", "Startup", - "Pre-Release", "Sponsor", + "Workspace Extension", + "Local Extension", "This publisher has verified ownership of {0}", "Latest version:", "Activation time", @@ -28250,42 +31086,63 @@ "The icon color for pre-release extension.", "The icon color for extension sponsor." ], - "vs/workbench/contrib/extensions/browser/extensionsViewer": [ - "Error", - "Unknown Extension:", - "Extensions" + "vs/workbench/contrib/extensions/browser/exeBasedRecommendations": [ + "This extension is recommended because you have {0} installed." ], "vs/workbench/contrib/extensions/browser/workspaceRecommendations": [ + "This extension is recommended by users of the current workspace.", "This extension is recommended by users of the current workspace." ], "vs/workbench/contrib/extensions/browser/fileBasedRecommendations": [ "This extension is recommended based on the files you recently opened.", "the {0} language" ], - "vs/workbench/contrib/extensions/browser/exeBasedRecommendations": [ - "This extension is recommended because you have {0} installed." - ], "vs/workbench/contrib/extensions/browser/configBasedRecommendations": [ "This extension is recommended because of the current workspace configuration" ], "vs/workbench/contrib/extensions/browser/webRecommendations": [ "This extension is recommended for {0} for the Web" ], - "vs/platform/files/browser/htmlFileSystemProvider": [ - "Rename is only supported for files.", - "Insufficient permissions. Please retry and allow the operation." + "vs/platform/terminal/common/terminalLogService": [ + "Terminal" ], - "vs/workbench/contrib/terminal/browser/terminalService": [ - "Do you want to terminate the active terminal session?", - "Do you want to terminate the {0} active terminal sessions?", - "&&Terminate", - "This shell is open to a {0}local{1} folder, NOT to the virtual folder", - "This shell is running on your {0}local{1} machine, NOT on the connected remote machine" + "vs/workbench/contrib/terminal/browser/terminalIcons": [ + "View icon of the terminal view.", + "Icon for rename in the terminal quick menu.", + "Icon for killing a terminal instance.", + "Icon for creating a new terminal instance.", + "Icon for creating a new terminal profile.", + "Icon for a terminal decoration mark.", + "Icon for a terminal decoration of a command that was incomplete.", + "Icon for a terminal decoration of a command that errored.", + "Icon for a terminal decoration of a command that was successful.", + "Icon for removing a terminal command from command history.", + "Icon for viewing output of a terminal command.", + "Icon for toggling fuzzy search of command history." ], "vs/workbench/contrib/terminal/browser/terminalActions": [ "Show Tabs", "Select current working directory for new terminal", "Open Help", + "Only files on disk can be run in the terminal", + "There are no unattached terminals to attach to", + "The sequence of text to send to the terminal", + "The directory to start the terminal at", + "The new name for the terminal", + "No name argument provided", + "Insufficient terminals for the join action", + "All terminals are joined already", + "Sticky Scroll", + "&&Sticky Scroll", + "Providing no name will reset it to the default value", + "The name of the profile to create", + "Where to create the terminal", + "Create the terminal in the terminal view", + "Create the terminal in the editor", + "Select current working directory for new terminal", + "(Overriden) {0}", + "Select current working directory for new terminal", + "Enter terminal name", "Create New Terminal (In Active Workspace)", "Create New Terminal in Editor Area", "Create New Terminal in Editor Area", @@ -28293,8 +31150,11 @@ "Focus Previous Terminal in Terminal Group", "Focus Next Terminal in Terminal Group", "Run Recent Command...", + "Copy Last Command", "Copy Last Command Output", + "Copy Last Command and Output", "Go to Recent Directory...", + "Goes to a recent folder", "Resize Terminal Left", "Resize Terminal Right", "Resize Terminal Up", @@ -28304,7 +31164,6 @@ "Focus Previous Terminal Group", "Run Selected Text In Active Terminal", "Run Active File In Active Terminal", - "Only files on disk can be run in the terminal", "Scroll Down (Line)", "Scroll Down (Page)", "Scroll to Bottom", @@ -28314,23 +31173,14 @@ "Clear Selection", "Detach Session", "Attach to Session", - "There are no unattached terminals to attach to", "Switch Active Terminal", - "Scroll To Previous Command", - "Scroll To Next Command", "Select To Previous Command", "Select To Next Command", "Select To Previous Line", "Select To Next Line", - "The sequence of text to send to the terminal", - "The directory to start the terminal at", - "The new name for the terminal", - "No name argument provided", "Relaunch Active Terminal", "Join Terminals", - "Join Terminals", - "Insufficient terminals for the join action", - "All terminals are joined already", + "Join Terminals...", "Split Terminal (In Active Workspace)", "Select All", "Create New Terminal", @@ -28341,33 +31191,97 @@ "Select Default Profile", "Configure Terminal Settings", "Set Fixed Dimensions", - "Toggle Size to Content Width", "Clear Previous Session History", - "Select the Previous Suggestion", - "Select the Previous Page Suggestion", - "Select the Next Suggestion", - "Select the Next Page Suggestion", - "Accept Selected Suggestion", - "Hide Suggest Widget", + "Toggle Sticky Scroll", "Copy Selection", "Copy and Clear Selection", "Copy Selection as HTML", "Paste into Active Terminal", "Paste Selection into Active Terminal", "Switch Terminal", - "Providing no name will reset it to the default value", - "Create New Terminal (With Profile)", - "The name of the profile to create", - "Select current working directory for new terminal", - "(Overriden) {0}", - "Select current working directory for new terminal", - "Enter terminal name" + "Create New Terminal (With Profile)" + ], + "vs/workbench/contrib/terminal/browser/terminalMenus": [ + "&&New Terminal", + "&&Split Terminal", + "Run &&Active File", + "Run &&Selected Text", + "Copy", + "Copy as HTML", + "Paste", + "Clear", + "Select All", + "Copy", + "Copy as HTML", + "Paste", + "Clear", + "Select All", + "New Terminal With Profile...", + "Configure Terminal Settings", + "Run Task...", + "Configure Tasks...", + "Clear Terminal", + "Run Active File", + "Run Selected Text", + "Rename...", + "Change Icon...", + "Change Color...", + "Join Terminals", + "{0} (Default)", + "{0} (Default)", + "{0} (Default)", + "Split Terminal", + "Launch Profile...", + "Select Default Profile", + "Switch Terminal" + ], + "vs/workbench/contrib/terminal/browser/terminalWslRecommendationContribution": [ + "The '{0}' extension is recommended for opening a terminal in WSL.", + "Install" ], "vs/workbench/contrib/terminal/browser/terminalQuickAccess": [ "Create New Terminal", - "Create New Terminal With Profile", + "Create New Terminal With Profile...", "Rename Terminal" ], + "vs/workbench/contrib/terminal/browser/terminalService": [ + "Do you want to terminate the active terminal session?", + "Do you want to terminate the {0} active terminal sessions?", + "&&Terminate", + "This shell is open to a {0}local{1} folder, NOT to the virtual folder", + "This shell is running on your {0}local{1} machine, NOT on the connected remote machine" + ], + "vs/workbench/contrib/terminal/common/terminalStrings": [ + "Terminal", + "New Terminal", + "Do Not Show Again", + "current session", + "previous session", + "Task", + "Local", + "Kill", + "Split", + "Terminal", + "Focus Terminal", + "Focus Terminal and Hide Accessible Buffer", + "Kill Terminal", + "Move Terminal into Editor Area", + "Move Terminal into New Window", + "Move Terminal into Panel", + "Change Icon...", + "Change Color...", + "Split Terminal", + "Unsplit Terminal", + "Rename...", + "Toggle Size to Content Width", + "Focus Hover", + "Send Custom Sequence To Terminal", + "Create New Terminal Starting in a Custom Working Directory", + "Rename the Currently Active Terminal", + "Sticky Scroll", + "Scroll To Previous Command", + "Scroll To Next Command" + ], "vs/workbench/contrib/terminal/common/terminalConfiguration": [ "the terminal's current working directory", "the terminal's current working directory, displayed for multi-root workspaces or in a single root workspace when the value differs from the initial working directory. On Windows, this will only be displayed when shell integration is enabled.", @@ -28412,7 +31326,10 @@ "Controls whether to force selection when using Option+click on macOS. This will force a regular (line) selection and disallow the use of column selection mode. This enables copying and pasting using the regular terminal selection, for example, when mouse mode is enabled in tmux.", "If enabled, alt/option + click will reposition the prompt cursor to underneath the mouse when {0} is set to {1} (the default value). This may not work reliably depending on your shell.", "Controls whether text selected in the terminal will be copied to the clipboard.", - "Show a warning dialog when pasting multiple lines into the terminal. The dialog does not show when:\n\n- Bracketed paste mode is enabled (the shell supports multi-line paste natively)\n- The paste is handled by the shell's readline (in the case of pwsh)", + "Controls whether to show a warning dialog when pasting multiple lines into the terminal.", + "Enable the warning but do not show it when:\n\n- Bracketed paste mode is enabled (the shell supports multi-line paste natively)\n- The paste is handled by the shell's readline (in the case of pwsh)", + "Always show the warning if the text contains a new line.", + "Never show the warning.", "Controls whether bold text in the terminal will always use the \"bright\" ANSI color variant.", "Controls the font family of the terminal. Defaults to {0}'s value.", "Controls the font size in pixels of the terminal.", @@ -28448,154 +31365,91 @@ "Select the word under the cursor and show the context menu.", "Do nothing and pass event to terminal.", "Controls how terminal reacts to right click.", + "The platform default to focus the terminal. On Linux this will also paste the selection.", + "Paste on middle click.", + "Controls how terminal reacts to middle click.", "An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process. This may be particularly useful in workspace settings if the root directory is not a convenient cwd.", "Controls whether to confirm when the window closes if there are active terminal sessions.", "Never confirm.", "Always confirm if there are terminals.", "Confirm if there are any terminals that have child processes.", - "Controls whether to confirm killing terminals when they have child processes. When set to editor, terminals in the editor area will be marked as changed when they have child processes. Note that child process detection may not work well for shells like Git Bash which don't run their processes as child processes of the shell.", - "Never confirm.", - "Confirm if the terminal is in the editor.", - "Confirm if the terminal is in the panel.", - "Confirm if the terminal is either in the editor or panel.", - "Controls whether the terminal bell is enabled. This shows up as a visual bell next to the terminal's name.", - "A set of command IDs whose keybindings will not be sent to the shell but instead always be handled by VS Code. This allows keybindings that would normally be consumed by the shell to act instead the same as when the terminal is not focused, for example `Ctrl+P` to launch Quick Open.\n\n \n\nMany commands are skipped by default. To override a default and pass that command's keybinding to the shell instead, add the command prefixed with the `-` character. For example add `-workbench.action.quickOpen` to allow `Ctrl+P` to reach the shell.\n\n \n\nThe following list of default skipped commands is truncated when viewed in Settings Editor. To see the full list, {1} and search for the first command from the list below.\n\n \n\nDefault Skipped Commands:\n\n{0}", - "open the default settings JSON", - "Open Default Settings (JSON)", - "Whether or not to allow chord keybindings in the terminal. Note that when this is true and the keystroke results in a chord it will bypass {0}, setting this to false is particularly useful when you want ctrl+k to go to your shell (not VS Code).", - "Whether to allow menubar mnemonics (for example Alt+F) to trigger the open of the menubar. Note that this will cause all alt keystrokes to skip the shell when true. This does nothing on macOS.", - "Object with environment variables that will be added to the VS Code process to be used by the terminal on macOS. Set to `null` to delete the environment variable.", - "Object with environment variables that will be added to the VS Code process to be used by the terminal on Linux. Set to `null` to delete the environment variable.", - "Object with environment variables that will be added to the VS Code process to be used by the terminal on Windows. Set to `null` to delete the environment variable.", - "Whether to display the environment changes indicator on each terminal which explains whether extensions have made, or want to make changes to the terminal's environment.", - "Disable the indicator.", - "Enable the indicator.", - "Only show the warning indicator when a terminal's environment is 'stale', not the information indicator that shows a terminal has had its environment modified by an extension.", - "Whether to relaunch terminals automatically if extensions want to contribute to their environment and have not been interacted with yet.", - "Controls whether to show the alert \"The terminal process terminated with exit code\" when exit code is non-zero.", - "Controls the working directory a split terminal starts with.", - "A new split terminal will use the workspace root as the working directory. In a multi-root workspace a choice for which root folder to use is offered.", - "A new split terminal will use the working directory that the parent terminal started with.", - "On macOS and Linux, a new split terminal will use the working directory of the parent terminal. On Windows, this behaves the same as initial.", - "Whether to use ConPTY for Windows terminal process communication (requires Windows 10 build number 18309+). Winpty will be used if this is false.", - "A string containing all characters to be considered word separators when double-clicking to select word and in the fallback 'word' link detection. Since this is used for link detection, including characters such as `:` that are used when detecting links will cause the line and column part of links like `file:10:5` to be ignored.", - "Whether to enable file links in terminals. Links can be slow when working on a network drive in particular because each file link is verified against the file system. Changing this will take effect only in new terminals.", - "Always off.", - "Always on.", - "Enable only when not in a remote workspace.", - "Version 6 of Unicode. This is an older version which should work better on older systems.", - "Version 11 of Unicode. This version provides better support on modern systems that use modern versions of Unicode.", - "Controls what version of Unicode to use when evaluating the width of characters in the terminal. If you experience emoji or other wide characters not taking up the right amount of space or backspace either deleting too much or too little then you may want to try tweaking this setting.", - "Length of network delay, in milliseconds, where local edits will be echoed on the terminal without waiting for server acknowledgement. If '0', local echo will always be on, and if '-1' it will be disabled.", - "When local echo should be enabled. This will override {0}", - "Always enabled", - "Always disabled", - "Enabled only for remote workspaces", - "Local echo will be disabled when any of these program names are found in the terminal title.", - "Terminal style of locally echoed text; either a font style or an RGB color.", - "Persist terminal sessions/history for the workspace across window reloads.", - "When the terminal process must be shut down (for example on window or application close), this determines when the previous terminal session contents/history should be restored and processes be recreated when the workspace is next opened.\n\nCaveats:\n\n- Restoring of the process current working directory depends on whether it is supported by the shell.\n- Time to persist the session during shutdown is limited, so it may be aborted when using high-latency remote connections.", - "Revive the processes after the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu).", - "Revive the processes after the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), or when the window is closed.", - "Never restore the terminal buffers or recreate the process.", - "Whether to hide the terminal view on startup, avoiding creating a terminal when there are no persistent sessions.", - "Never hide the terminal view on startup.", - "Only hide the terminal when there are no persistent sessions restored.", - "Always hide the terminal, even when there are persistent sessions restored.", - "Whether to draw custom glyphs for block element and box drawing characters instead of using the font, which typically yields better rendering with continuous lines. Note that this doesn't work when {0} is disabled.", - "A set of messages that, when encountered in the terminal, will be automatically responded to. Provided the message is specific enough, this can help automate away common responses.\n\nRemarks:\n\n- Use {0} to automatically respond to the terminate batch job prompt on Windows.\n- The message includes escape sequences so the reply might not happen with styled text.\n- Each reply can only happen once every second.\n- Use {1} in the reply to mean the enter key.\n- To unset a default key, set the value to null.\n- Restart VS Code if new don't apply.", - "The reply to send to the process.", - "Determines whether or not shell integration is auto-injected to support features like enhanced command tracking and current working directory detection. \n\nShell integration works by injecting the shell with a startup script. The script gives VS Code insight into what is happening within the terminal.\n\nSupported shells:\n\n- Linux/macOS: bash, fish, pwsh, zsh\n - Windows: pwsh\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\n Note that the script injection may not work if you have custom arguments defined in the terminal profile, have enabled {1}, have a [complex bash `PROMPT_COMMAND`](https://code.visualstudio.com/docs/editor/integrated-terminal#_complex-bash-promptcommand), or other unsupported setup. To disable decorations, see {0}", - "When shell integration is enabled, adds a decoration for each command.", - "Show decorations in the gutter (left) and overview ruler (right)", - "Show gutter decorations to the left of the terminal", - "Show overview ruler decorations to the right of the terminal", - "Do not show decorations", - "Controls the number of recently used commands to keep in the terminal command history. Set to 0 to disable terminal command history.", - "Enables experimental terminal Intellisense suggestions for supported shells when {0} is set to {1}. If shell integration is installed manually, {2} needs to be set to {3} before calling the script.", - "Controls whether the terminal will scroll using an animation.", - "Controls whether the terminal will ignore bracketed paste mode even if the terminal was put into the mode, omitting the {0} and {1} sequences when pasting. This is useful when the shell is not respecting the mode which can happen in sub-shells for example.", - "Enables image support in the terminal, this will only work when {0} is enabled. Both sixel and iTerm's inline image protocol are supported on Linux and macOS, Windows support will light up automatically when ConPTY passes through the sequences. Images will currently not be restored between window reloads/reconnects.", - "Controls whether the terminal, accessible buffer, or neither will be focused after `Terminal: Run Selected Text In Active Terminal` has been run.", - "Always focus the terminal.", - "Always focus the accessible buffer.", - "Do nothing." - ], - "vs/workbench/contrib/terminal/browser/terminalMenus": [ - "&&New Terminal", - "&&Split Terminal", - "Run &&Active File", - "Run &&Selected Text", - "Copy", - "Copy as HTML", - "Paste", - "Clear", - "Select All", - "Copy", - "Copy as HTML", - "Paste", - "Clear", - "Select All", - "New Terminal With Profile", - "Select Default Profile", - "Configure Terminal Settings", - "Run Task...", - "Configure Tasks...", - "Switch Terminal", - "Clear Terminal", - "Run Active File", - "Run Selected Text", - "Rename...", - "Change Icon...", - "Change Color...", - "Toggle Size to Content Width", - "Join Terminals", - "{0} (Default)", - "{0} (Default)", - "{0} (Default)", - "Split Terminal" - ], - "vs/workbench/contrib/terminal/browser/terminalIcons": [ - "View icon of the terminal view.", - "Icon for rename in the terminal quick menu.", - "Icon for killing a terminal instance.", - "Icon for creating a new terminal instance.", - "Icon for creating a new terminal profile.", - "Icon for a terminal decoration mark.", - "Icon for a terminal decoration of a command that was incomplete.", - "Icon for a terminal decoration of a command that errored.", - "Icon for a terminal decoration of a command that was successful.", - "Icon for removing a terminal command from command history.", - "Icon for viewing output of a terminal command.", - "Icon for toggling fuzzy search of command history." - ], - "vs/workbench/contrib/terminal/common/terminalStrings": [ - "Terminal", - "New Terminal", - "Do Not Show Again", - "current session", - "previous session", - "Terminal", - "Focus Terminal", - "Focus Terminal and Hide Accessible Buffer", - "Kill Terminal", - "Kill", - "Move Terminal into Editor Area", - "Move Terminal into Panel", - "Change Icon...", - "Change Color...", - "Split Terminal", - "Split", - "Unsplit Terminal", - "Rename...", - "Toggle Size to Content Width", - "Focus Hover", - "Send Custom Sequence To Terminal", - "Create New Terminal Starting in a Custom Working Directory", - "Rename the Currently Active Terminal" - ], - "vs/platform/terminal/common/terminalLogService": [ - "Terminal" + "Controls whether to confirm killing terminals when they have child processes. When set to editor, terminals in the editor area will be marked as changed when they have child processes. Note that child process detection may not work well for shells like Git Bash which don't run their processes as child processes of the shell.", + "Never confirm.", + "Confirm if the terminal is in the editor.", + "Confirm if the terminal is in the panel.", + "Confirm if the terminal is either in the editor or panel.", + "This is now deprecated. Instead use the `terminal.integrated.enableVisualBell` and `accessibility.signals.terminalBell` settings.", + "Controls whether the visual terminal bell is enabled. This shows up next to the terminal's name.", + "A set of command IDs whose keybindings will not be sent to the shell but instead always be handled by VS Code. This allows keybindings that would normally be consumed by the shell to act instead the same as when the terminal is not focused, for example `Ctrl+P` to launch Quick Open.\n\n \n\nMany commands are skipped by default. To override a default and pass that command's keybinding to the shell instead, add the command prefixed with the `-` character. For example add `-workbench.action.quickOpen` to allow `Ctrl+P` to reach the shell.\n\n \n\nThe following list of default skipped commands is truncated when viewed in Settings Editor. To see the full list, {1} and search for the first command from the list below.\n\n \n\nDefault Skipped Commands:\n\n{0}", + "open the default settings JSON", + "Open Default Settings (JSON)", + "Whether or not to allow chord keybindings in the terminal. Note that when this is true and the keystroke results in a chord it will bypass {0}, setting this to false is particularly useful when you want ctrl+k to go to your shell (not VS Code).", + "Whether to allow menubar mnemonics (for example Alt+F) to trigger the open of the menubar. Note that this will cause all alt keystrokes to skip the shell when true. This does nothing on macOS.", + "Object with environment variables that will be added to the VS Code process to be used by the terminal on macOS. Set to `null` to delete the environment variable.", + "Object with environment variables that will be added to the VS Code process to be used by the terminal on Linux. Set to `null` to delete the environment variable.", + "Object with environment variables that will be added to the VS Code process to be used by the terminal on Windows. Set to `null` to delete the environment variable.", + "Whether to display the environment changes indicator on each terminal which explains whether extensions have made, or want to make changes to the terminal's environment.", + "Disable the indicator.", + "Enable the indicator.", + "Only show the warning indicator when a terminal's environment is 'stale', not the information indicator that shows a terminal has had its environment modified by an extension.", + "Whether to relaunch terminals automatically if extensions want to contribute to their environment and have not been interacted with yet.", + "Controls whether to show the alert \"The terminal process terminated with exit code\" when exit code is non-zero.", + "Controls the working directory a split terminal starts with.", + "A new split terminal will use the workspace root as the working directory. In a multi-root workspace a choice for which root folder to use is offered.", + "A new split terminal will use the working directory that the parent terminal started with.", + "On macOS and Linux, a new split terminal will use the working directory of the parent terminal. On Windows, this behaves the same as initial.", + "Whether to use ConPTY for Windows terminal process communication (requires Windows 10 build number 18309+). Winpty will be used if this is false.", + "A string containing all characters to be considered word separators when double-clicking to select word and in the fallback 'word' link detection. Since this is used for link detection, including characters such as `:` that are used when detecting links will cause the line and column part of links like `file:10:5` to be ignored.", + "Whether to enable file links in terminals. Links can be slow when working on a network drive in particular because each file link is verified against the file system. Changing this will take effect only in new terminals.", + "Always off.", + "Always on.", + "Enable only when not in a remote workspace.", + "An array of strings containing the URI schemes that the terminal is allowed to open links for. By default, only a small subset of possible schemes are allowed for security reasons.", + "Version 6 of Unicode. This is an older version which should work better on older systems.", + "Version 11 of Unicode. This version provides better support on modern systems that use modern versions of Unicode.", + "Controls what version of Unicode to use when evaluating the width of characters in the terminal. If you experience emoji or other wide characters not taking up the right amount of space or backspace either deleting too much or too little then you may want to try tweaking this setting.", + "Length of network delay, in milliseconds, where local edits will be echoed on the terminal without waiting for server acknowledgement. If '0', local echo will always be on, and if '-1' it will be disabled.", + "When local echo should be enabled. This will override {0}", + "Always enabled", + "Always disabled", + "Enabled only for remote workspaces", + "Local echo will be disabled when any of these program names are found in the terminal title.", + "Terminal style of locally echoed text; either a font style or an RGB color.", + "Persist terminal sessions/history for the workspace across window reloads.", + "When the terminal process must be shut down (for example on window or application close), this determines when the previous terminal session contents/history should be restored and processes be recreated when the workspace is next opened.\n\nCaveats:\n\n- Restoring of the process current working directory depends on whether it is supported by the shell.\n- Time to persist the session during shutdown is limited, so it may be aborted when using high-latency remote connections.", + "Revive the processes after the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu).", + "Revive the processes after the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), or when the window is closed.", + "Never restore the terminal buffers or recreate the process.", + "Whether to hide the terminal view on startup, avoiding creating a terminal when there are no persistent sessions.", + "Never hide the terminal view on startup.", + "Only hide the terminal when there are no persistent sessions restored.", + "Always hide the terminal, even when there are persistent sessions restored.", + "Whether to draw custom glyphs for block element and box drawing characters instead of using the font, which typically yields better rendering with continuous lines. Note that this doesn't work when {0} is disabled.", + "Whether to rescale glyphs horizontally that are a single cell wide but have glyphs that would overlap following cell(s). This typically happens for ambiguous width characters (eg. the roman numeral characters U+2160+) which aren't featured in monospace fonts. Emoji glyphs are never rescaled.", + "A set of messages that, when encountered in the terminal, will be automatically responded to. Provided the message is specific enough, this can help automate away common responses.\n\nRemarks:\n\n- Use {0} to automatically respond to the terminate batch job prompt on Windows.\n- The message includes escape sequences so the reply might not happen with styled text.\n- Each reply can only happen once every second.\n- Use {1} in the reply to mean the enter key.\n- To unset a default key, set the value to null.\n- Restart VS Code if new don't apply.", + "The reply to send to the process.", + "Determines whether or not shell integration is auto-injected to support features like enhanced command tracking and current working directory detection. \n\nShell integration works by injecting the shell with a startup script. The script gives VS Code insight into what is happening within the terminal.\n\nSupported shells:\n\n- Linux/macOS: bash, fish, pwsh, zsh\n - Windows: pwsh, git bash\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\n Note that the script injection may not work if you have custom arguments defined in the terminal profile, have enabled {1}, have a [complex bash `PROMPT_COMMAND`](https://code.visualstudio.com/docs/editor/integrated-terminal#_complex-bash-promptcommand), or other unsupported setup. To disable decorations, see {0}", + "When shell integration is enabled, adds a decoration for each command.", + "Show decorations in the gutter (left) and overview ruler (right)", + "Show gutter decorations to the left of the terminal", + "Show overview ruler decorations to the right of the terminal", + "Do not show decorations", + "Controls the number of recently used commands to keep in the terminal command history. Set to 0 to disable terminal command history.", + "Enables experimental terminal Intellisense suggestions for supported shells when {0} is set to {1}. If shell integration is installed manually, {2} needs to be set to {3} before calling the script.", + "This is an experimental setting and may break the terminal! Use at your own risk.", + "Controls whether the terminal will scroll using an animation.", + "Controls whether the terminal will ignore bracketed paste mode even if the terminal was put into the mode, omitting the {0} and {1} sequences when pasting. This is useful when the shell is not respecting the mode which can happen in sub-shells for example.", + "Enables image support in the terminal, this will only work when {0} is enabled. Both sixel and iTerm's inline image protocol are supported on Linux and macOS, Windows support will light up automatically when ConPTY passes through the sequences. Images will currently not be restored between window reloads/reconnects.", + "Controls whether the terminal, accessible buffer, or neither will be focused after `Terminal: Run Selected Text In Active Terminal` has been run.", + "Always focus the terminal.", + "Always focus the accessible buffer.", + "Do nothing.", + "Preserve the cursor position on reopen of the terminal's accessible view rather than setting it to the bottom of the buffer.", + "Focus the terminal accessible view when a command is executed.", + "Shows the current command at the top of the terminal.", + "Defines the maximum number of sticky lines to show. Sticky scroll lines will never exceed 40% of the viewport regardless of this setting.", + "Zoom the font of the terminal when using mouse wheel and holding `Cmd`.", + "Zoom the font of the terminal when using mouse wheel and holding `Ctrl`." ], "vs/workbench/contrib/terminal/browser/terminalTabbedView": [ "Move Tabs Right", @@ -28610,8 +31464,10 @@ "Command line: {0}" ], "vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp": [ - "The Focus Accessible Buffer ({0}) command enables screen readers to read terminal contents.", - "The Focus Accessible Buffer command enables screen readers to read terminal contents and is currently not triggerable by a keybinding.", + "The Focus Accessible Terminal View ({0}) command enables screen readers to read terminal contents.", + "The Focus Terminal Accessible View command enables screen readers to read terminal contents and is currently not triggerable by a keybinding.", + "Customize the behavior of the cursor when toggling between the terminal and accessible view with `terminal.integrated.accessibleViewPreserveCursorPosition.`", + "Enable `terminal.integrated.accessibleViewFocusOnCommandExecution` to automatically focus the terminal accessible view when a command is executed in the terminal.", "Consider using powershell instead of command prompt for an improved experience", "The terminal has a feature called shell integration that offers an enhanced experience and provides useful commands for screen readers such as:", "Go to Next Command ({0}) in the accessible view", @@ -28630,8 +31486,18 @@ "The Open Detected Link command enables screen readers to easily open links found in the terminal and is currently not triggerable by a keybinding.", "The Create New Terminal (With Profile) ({0}) command allows for easy terminal creation using a specific profile.", "The Create New Terminal (With Profile) command allows for easy terminal creation using a specific profile and is currently not triggerable by a keybinding.", - "Configure what gets focused after running selected text in the terminal with `{0}`.", - "Access accessibility settings such as `terminal.integrated.tabFocusMode` via the Preferences: Open Accessibility Settings command." + "Configure what gets focused after running selected text in the terminal with `{0}`." + ], + "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager": [ + "Opening URIs can be insecure, do you want to allow opening links with the scheme {0}?", + "Allow {0}", + "option + click", + "alt + click", + "cmd + click", + "ctrl + click", + "Follow link", + "Follow link using forwarded port", + "Link" ], "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick": [ "Url", @@ -28644,19 +31510,23 @@ "Folder", "Workspace Search" ], - "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager": [ - "option + click", - "alt + click", - "cmd + click", - "ctrl + click", - "Follow link", - "Follow link using forwarded port", - "Link" - ], - "vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon": [ - "Run: {0}", - "Open: {0}", - "Quick Fix" + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp": [ + "Inline chat occurs within a terminal. It is useful for suggesting terminal commands. Keep in mind that AI generated code may be incorrect.", + "It can be activated using the command: Terminal: Start Chat ({0}), which will focus the input box.", + "The input box is where the user can type a request and can make the request ({0}). The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.", + "The input box is where the user can type a request and can make the request by tabbing to the Make Request button, which is not currently triggerable via keybindings. The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.", + "The response can be inspected in the accessible view ({0}).", + "With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.", + "Reach the response from the input box ({0}).", + "Reach the response from the input box by tabbing or assigning a keybinding for the command: Focus Terminal Response.", + "Reach the input box from the response ({0}).", + "Reach the response from the input box by shift+tabbing or assigning a keybinding for the command: Focus Terminal Input.", + "With focus in the input box or command editor, the Terminal: Run Chat Command ({0}) action.", + "Run a command by tabbing to the button as the action is currently not triggerable by a keybinding.", + "With focus in the input box command editor, the Terminal: Insert Chat Command ({0}) action.", + "Insert a command by tabbing to the button as the action is currently not triggerable by a keybinding.", + "Use tab to reach conditional parts like commands, status, message responses and more.", + "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring." ], "vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixBuiltinActions": [ "Free port {0}", @@ -28670,6 +31540,35 @@ "The command exit result to match on", "The kind of the resulting quick fix. This changes how the quick fix is presented. Defaults to {0}." ], + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions": [ + "Start in Terminal", + "Close Chat", + "Focus Terminal Response", + "Focus Terminal Input", + "Discard", + "Discards the terminal current chat response, hide the chat widget, and clear the chat input.", + "Run Chat Command", + "Run", + "Run First Chat Command", + "Run First", + "Insert Chat Command", + "Insert", + "Insert First Chat Command", + "Insert First", + "View in Chat", + "Make Chat Request", + "Cancel Chat", + "Report Issue" + ], + "vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon": [ + "Run: {0}", + "Open: {0}", + "Quick Fix" + ], + "vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry": [ + "The background color of the sticky scroll overlay in the terminal.", + "The background color of the sticky scroll overlay in the terminal when hovered." + ], "vs/workbench/contrib/tasks/common/jsonSchemaCommon": [ "Additional command options", "The current working directory of the executed program or script. If omitted Code's current workspace root is used.", @@ -28735,9 +31634,12 @@ "Optional arguments passed to the command." ], "vs/workbench/contrib/remote/browser/explorerViewItems": [ - "Switch Remote", "Switch Remote" ], + "vs/base/browser/ui/findinput/replaceInput": [ + "input", + "Preserve Case" + ], "vs/workbench/contrib/snippets/browser/commands/abstractSnippetsActions": [ "Snippets" ], @@ -28760,10 +31662,14 @@ "{0}, {1}", "{0}, {1}" ], + "vs/workbench/contrib/update/browser/releaseNotesEditor": [ + "Release Notes: {0}", + "unassigned", + "Show release notes after an update" + ], "vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent": [ "Icon used for the setup category of welcome page", "Icon used for the beginner category of welcome page", - "Icon used for the intermediate category of welcome page", "New File...", "Open a new untitled text file, notebook, or custom editor.", "Open...", @@ -28785,22 +31691,25 @@ "Open Tunnel...", "Connect to a remote machine through a Tunnel", "Get Started with VS Code", - "Discover the best customizations to make VS Code yours.", - "Choose the look you want", - "The right color palette helps you focus on your code, is easy on your eyes, and is simply more fun to use.\n{0}", + "Customize your editor, learn the basics, and start coding", + "Choose your theme", + "The right theme helps you focus on your code, is easy on your eyes, and is simply more fun to use.\n{0}", "Browse Color Themes", - "Sync to and from other devices", - "Keep your essential VS Code customizations backed up and updated across all your devices.\n{0}", - "Enable Settings Sync", - "One shortcut to access everything", - "Commands are the keyboard way to accomplish any task in VS Code. **Practice** by looking up your frequent ones to save time.\n{0}\n__Try searching for 'view toggle'.__", - "Open Command Palette", - "Limitless extensibility", + "Code with extensions", "Extensions are VS Code's power-ups. A growing number are becoming available in the web.\n{0}", "Browse Popular Web Extensions", "Rich support for all your languages", "Code smarter with syntax highlighting, code completion, linting and debugging. While many languages are built-in, many more can be added as extensions.\n{0}", "Browse Language Extensions", + "Tune your settings", + "Customize every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", + "Open Settings", + "Sync settings across devices", + "Keep your essential customizations backed up and updated across all your devices.\n{0}", + "Backup and Sync Settings", + "Unlock productivity with the Command Palette ", + "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", + "Open Command Palette", "Open up your code", "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", "Pick a Folder", @@ -28810,26 +31719,29 @@ "Quickly navigate between your files", "Navigate between files in an instant with one keystroke. Tip: Open multiple files by pressing the right arrow key.\n{0}", "Quick Open a File", + "Watch video tutorials", + "Watch the first in a series of short & practical video tutorials for VS Code's key features.\n{0}", + "Watch Tutorial", "Get Started with VS Code for the Web", - "Discover the best customizations to make VS Code for the Web yours.", - "Choose the look you want", - "The right color palette helps you focus on your code, is easy on your eyes, and is simply more fun to use.\n{0}", + "Customize your editor, learn the basics, and start coding", + "Choose your theme", + "The right theme helps you focus on your code, is easy on your eyes, and is simply more fun to use.\n{0}", "Browse Color Themes", - "Sync to and from other devices", - "Keep your essential VS Code customizations backed up and updated across all your devices.\n{0}", - "Enable Settings Sync", - "One shortcut to access everything", - "Commands are the keyboard way to accomplish any task in VS Code. **Practice** by looking up your frequent ones to save time.\n{0}\n__Try searching for 'view toggle'.__", - "Open Command Palette", "Just the right amount of UI", "The full menu bar is available in the dropdown menu to make room for your code. Toggle its appearance for faster access. \n{0}", "Toggle Menu Bar", - "Limitless extensibility", + "Code with extensions", "Extensions are VS Code's power-ups. A growing number are becoming available in the web.\n{0}", "Browse Popular Web Extensions", "Rich support for all your languages", "Code smarter with syntax highlighting, code completion, linting and debugging. While many languages are built-in, many more can be added as extensions.\n{0}", "Browse Language Extensions", + "Sync settings across devices", + "Keep your essential customizations backed up and updated across all your devices.\n{0}", + "Backup and Sync Settings", + "Unlock productivity with the Command Palette ", + "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", + "Open Command Palette", "Open up your code", "You're all set to start coding. You can open a local project or a remote repository to get your files into VS Code.\n{0}\n{1}", "Open Folder", @@ -28838,34 +31750,13 @@ "Navigate between files in an instant with one keystroke. Tip: Open multiple files by pressing the right arrow key.\n{0}", "Quick Open a File", "Learn the Fundamentals", - "Jump right into VS Code and get an overview of the must-have features.", - "Redefine your editing skills", - "Want to code faster and smarter? Practice powerful code editing features in the interactive playground.\n{0}", - "Open Editor Playground", - "Convenient built-in terminal", - "Quickly run shell commands and monitor build output, right next to your code.\n{0}", - "Show Terminal Panel", - "Limitless extensibility", + "Get an overview of the most essential features", + "Code with extensions", "Extensions are VS Code's power-ups. They range from handy productivity hacks, expanding out-of-the-box features, to adding completely new capabilities.\n{0}", - "Browse Recommended Extensions", - "Tune your settings", - "Tweak every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", - "Tweak my Settings", - "Customize VS Code with Profiles", - "Profiles let you create sets of VS Code customizations that include settings, extensions and UI state. Create your own profile from scratch or use the predefined set of profile templates for your specific workflow.\n{0}", - "Try Profiles", - "Safely browse and edit code", - "{0} lets you decide whether your project folders should **allow or restrict** automatic code execution __(required for extensions, debugging, etc)__.\nOpening a file/folder will prompt to grant trust. You can always {1} later.", - "Workspace Trust", - "enable trust", - "Lean back and learn", - "Watch the first in a series of short & practical video tutorials for VS Code's key features.\n{0}", - "Watch Tutorial", - "Boost your Productivity", - "Optimize your development workflow with these tips & tricks.", - "Side by side editing", - "Make the most of your screen estate by opening files side by side, vertically and horizontally.\n{0}", - "Split Editor", + "Browse Popular Extensions", + "Built-in terminal", + "Quickly run shell commands and monitor build output, right next to your code.\n{0}", + "Open Terminal", "Watch your code in action", "Accelerate your edit, build, test, and debug loop by setting up a launch configuration.\n{0}", "Run your Project", @@ -28887,15 +31778,14 @@ "Customize your shortcuts", "Once you have discovered your favorite commands, create custom keyboard shortcuts for instant access.\n{0}", "Keyboard Shortcuts", + "Safely browse and edit code", + "{0} lets you decide whether your project folders should **allow or restrict** automatic code execution __(required for extensions, debugging, etc)__.\nOpening a file/folder will prompt to grant trust. You can always {1} later.", + "Workspace Trust", + "enable trust", "Customize Notebooks", "Select the layout for your notebooks", "Get notebooks to feel just the way you prefer" ], - "vs/workbench/contrib/update/browser/releaseNotesEditor": [ - "Release Notes: {0}", - "unassigned", - "Show release notes after an update" - ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedExtensionPoint": [ "Title", "Contribute walkthroughs to help users getting started with your extension.", @@ -28935,12 +31825,6 @@ "Mark step done when the specified command is executed.", "Context key expression to control the visibility of this step." ], - "vs/workbench/contrib/welcomeWalkthrough/common/walkThroughUtils": [ - "Background color for the embedded editors on the Interactive Playground." - ], - "vs/workbench/contrib/welcomeGettingStarted/browser/featuredExtensionService": [ - "Recommended" - ], "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors": [ "Background color for the Welcome page.", "Background color for the tiles on the Welcome page.", @@ -28950,6 +31834,19 @@ "Background color for the Welcome page progress bars.", "Foreground color of the heading of each walkthrough step" ], + "vs/workbench/contrib/welcomeWalkthrough/common/walkThroughUtils": [ + "Background color for the embedded editors on the Interactive Playground." + ], + "vs/workbench/contrib/callHierarchy/browser/callHierarchyTree": [ + "Call Hierarchy", + "calls from {0}", + "callers of {0}" + ], + "vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree": [ + "Type Hierarchy", + "supertypes of {0}", + "subtypes of {0}" + ], "vs/editor/contrib/symbolIcons/browser/symbolIcons": [ "The foreground color for array symbols. These symbols appear in the outline, breadcrumb, and suggest widget.", "The foreground color for boolean symbols. These symbols appear in the outline, breadcrumb, and suggest widget.", @@ -28985,24 +31882,9 @@ "The foreground color for unit symbols. These symbols appear in the outline, breadcrumb, and suggest widget.", "The foreground color for variable symbols. These symbols appear in the outline, breadcrumb, and suggest widget." ], - "vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree": [ - "Type Hierarchy", - "supertypes of {0}", - "subtypes of {0}" - ], - "vs/workbench/contrib/callHierarchy/browser/callHierarchyTree": [ - "Call Hierarchy", - "calls from {0}", - "callers of {0}" - ], "vs/workbench/contrib/userDataSync/browser/userDataSyncViews": [ - "Conflicts", - "Synced Machines", "Edit Name", "Turn off Settings Sync", - "Sync Activity (Remote)", - "Sync Activity (Local)", - "Sync Activity (Developer)", "Load Sync Activity", "Select Sync Activity File or Folder", "Show raw JSON sync data", @@ -29012,7 +31894,6 @@ "{0} (Local)", "Restore", "Would you like to replace your current {0} with selected?", - "Troubleshoot", "Reset Synced Data", "{0} ↔ {1}", "Current", @@ -29028,16 +31909,13 @@ "Machine name should be unique and not empty", "Logs", "Last Synced Remotes", - "Current" - ], - "vs/workbench/browser/parts/notifications/notificationsList": [ - "Inspect the response in the accessible view with {0}", - "Inspect the response in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding", - "{0}, notification, {1}", - "{0}, notification", - "{0}, source: {1}, notification, {2}", - "{0}, source: {1}, notification", - "Notifications List" + "Current", + "Conflicts", + "Synced Machines", + "Sync Activity (Remote)", + "Sync Activity (Local)", + "Sync Activity (Developer)", + "Troubleshoot" ], "vs/workbench/browser/parts/notifications/notificationsActions": [ "Icon for the clear action in notifications.", @@ -29050,15 +31928,46 @@ "Clear Notification", "Clear All Notifications", "Toggle Do Not Disturb Mode", + "Toggle Do Not Disturb Mode By Source...", + "Configure Do Not Disturb...", "Hide Notifications", "Expand Notification", "Collapse Notification", "More Actions...", "Copy Text" ], + "vs/workbench/browser/parts/notifications/notificationsList": [ + "Inspect the response in the accessible view with {0}", + "Inspect the response in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding", + "{0}, notification, {1}", + "{0}, notification", + "{0}, source: {1}, notification, {2}", + "{0}, source: {1}, notification", + "Notifications List" + ], "vs/workbench/services/textfile/common/textFileSaveParticipant": [ "Saving '{0}'" ], + "vs/workbench/browser/parts/titlebar/menubarControl": [ + "&&File", + "&&Edit", + "&&Selection", + "&&View", + "&&Go", + "&&Terminal", + "&&Help", + "Preferences", + "Accessibility support is enabled for you. For the most accessible experience, we recommend the custom title bar style.", + "Open Settings", + "Check for &&Updates...", + "Checking for Updates...", + "D&&ownload Update", + "Downloading Update...", + "Install &&Update...", + "Installing Update...", + "Restart to &&Update", + "Focus Application Menu" + ], "vs/workbench/browser/parts/titlebar/commandCenterControl": [ "Search", "{0} {1}", @@ -29067,10 +31976,43 @@ "Search {0} — {1}", "Command Center" ], - "vs/workbench/browser/parts/titlebar/windowTitle": [ - "[Administrator]", - "[Superuser]", - "[Extension Development Host]" + "vs/workbench/browser/parts/editor/editorTabsControl": [ + "Editor actions", + "{0} (+{1})" + ], + "vs/workbench/browser/parts/globalCompositeBar": [ + "Accounts icon in the view bar.", + "Hide Accounts", + "Manage", + "Accounts", + "Accounts", + "Loading...", + "{0} is currently unavailable", + "Manage Trusted Extensions", + "Sign Out", + "You are not signed in to any accounts", + "Manage", + "Manage {0} (Profile)", + "Hide Accounts", + "Accounts", + "Manage" + ], + "vs/workbench/browser/parts/titlebar/titlebarActions": [ + "Command Center", + "Toggle visibility of the Command Center in title bar", + "Layout Controls", + "Toggle visibility of the Layout Controls in title bar", + "Hide Custom Title Bar", + "Hide Custom Title Bar In Full Screen", + "Custom Title Bar", + "Editor Actions", + "Accounts", + "Accounts", + "Manage", + "Manage", + "Show Custom Title Bar", + "Hide Custom Title Bar", + "Hide Custom Title Bar In Full Screen" ], "vs/workbench/services/workingCopy/common/storedFileWorkingCopy": [ "Failed to save '{0}': The content of the file is newer. Do you want to overwrite the file with your changes?", @@ -29102,73 +32044,45 @@ "vs/workbench/contrib/webview/browser/webviewElement": [ "Error loading webview: {0}" ], + "vs/editor/browser/widget/multiDiffEditor/colors": [ + "The background color of the diff editor's header", + "The background color of the multi file diff editor", + "The border color of the multi file diff editor" + ], + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChat": [ + "Whether the chat view is focused.", + "Whether the chat view is visible.", + "Whether there is an active chat request.", + "Whether the chat input has text.", + "Whether the terminal chat agent has been registered.", + "Whether the chat response contains a code block.", + "Whether the chat response contains multiple code blocks.", + "Whether the response supports issue reporting", + "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string." + ], "vs/workbench/api/common/extHostDiagnostics": [ "Not showing {0} further errors and warnings." ], + "vs/workbench/api/common/extHostNotebook": [ + "Unable to modify read-only file '{0}'", + "File Modified Since" + ], "vs/workbench/api/common/extHostLanguageFeatures": [ "Paste using '{0}' extension", "Drop using '{0}' extension" ], - "vs/workbench/api/common/extHostProgress": [ - "{0} (Extension)" - ], "vs/workbench/api/common/extHostStatusBar": [ - "{0} (Extension)", - "Extension Status" - ], - "vs/workbench/api/common/extHostTreeViews": [ - "Element with id {0} is already registered" - ], - "vs/workbench/api/common/extHostNotebook": [ - "Unable to modify read-only file '{0}'", - "File Modified Since" + "{0} (Extension)", + "Extension Status" ], - "vs/workbench/api/common/extHostChat": [ - "Provider returned null response", - "Error from provider: {0}" + "vs/workbench/api/common/extHostTreeViews": [ + "Element with id {0} is already registered" ], "vs/base/browser/ui/findinput/findInputToggles": [ "Match Case", "Match Whole Word", "Use Regular Expression" ], - "vs/editor/browser/widget/diffEditor/accessibleDiffViewer": [ - "Icon for 'Insert' in accessible diff viewer.", - "Icon for 'Remove' in accessible diff viewer.", - "Icon for 'Close' in accessible diff viewer.", - "Close", - "Accessible Diff Viewer. Use arrow up and down to navigate.", - "no lines changed", - "1 line changed", - "{0} lines changed", - "Difference {0} of {1}: original line {2}, {3}, modified line {4}, {5}", - "blank", - "{0} unchanged line {1}", - "{0} original line {1} modified line {2}", - "+ {0} modified line {1}", - "- {0} original line {1}" - ], - "vs/editor/browser/widget/diffEditor/movedBlocksLines": [ - "Code moved with changes to line {0}-{1}", - "Code moved with changes from line {0}-{1}", - "Code moved to line {0}-{1}", - "Code moved from line {0}-{1}" - ], - "vs/editor/browser/widget/diffEditor/hideUnchangedRegionsFeature": [ - "Fold Unchanged Region", - "Click or drag to show more above", - "Show all", - "Click or drag to show more below", - "{0} hidden lines", - "Double click to unfold" - ], - "vs/editor/browser/widget/diffEditor/diffEditorEditors": [ - " use {0} to open the accessibility help." - ], - "vs/editor/browser/widget/diffEditor/colors": [ - "The border color for text that got moved in the diff editor.", - "The active border color for text that got moved in the diff editor." - ], "vs/editor/browser/controller/textAreaHandler": [ "editor", "The editor is not accessible at this time.", @@ -29176,15 +32090,6 @@ "{0} To enable screen reader optimized mode, open the quick pick with {1} and run the command Toggle Screen Reader Accessibility Mode, which is currently not triggerable via keyboard.", "{0} Please assign a keybinding for the command Toggle Screen Reader Accessibility Mode by accessing the keybindings editor with {1} and run it." ], - "vs/platform/actionWidget/browser/actionWidget": [ - "Background color for toggled action items in action bar.", - "Whether the action widget list is visible", - "Hide action widget", - "Select previous action", - "Select next action", - "Accept selected action", - "Preview selected action" - ], "vs/editor/contrib/codeAction/browser/codeActionMenu": [ "More Actions...", "Quick Fix", @@ -29195,6 +32100,15 @@ "Surround With", "Source Action" ], + "vs/platform/actionWidget/browser/actionWidget": [ + "Background color for toggled action items in action bar.", + "Whether the action widget list is visible", + "Hide action widget", + "Select previous action", + "Select next action", + "Accept selected action", + "Preview selected action" + ], "vs/editor/contrib/colorPicker/browser/colorPickerWidget": [ "Click to toggle color options (rgb/hsl/hex)", "Icon to close the color picker" @@ -29258,19 +32172,37 @@ "Icon for more information in the suggest widget.", "Read More" ], - "vs/workbench/contrib/comments/common/commentModel": [ - "There are no comments in this workspace yet." + "vs/workbench/contrib/chat/browser/chatFollowups": [ + "Follow up question: {0}" ], - "vs/workbench/contrib/comments/browser/commentsViewActions": [ - "Focus Comments view", - "Clear filter text", - "Focus comments filter", - "Toggle Unresolved Comments", - "Comments", - "Show Unresolved", - "Toggle Resolved Comments", - "Comments", - "Show Resolved" + "vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer": [ + "Icon for 'Insert' in accessible diff viewer.", + "Icon for 'Remove' in accessible diff viewer.", + "Icon for 'Close' in accessible diff viewer.", + "Close", + "Accessible Diff Viewer. Use arrow up and down to navigate.", + "no lines changed", + "1 line changed", + "{0} lines changed", + "Difference {0} of {1}: original line {2}, {3}, modified line {4}, {5}", + "blank", + "{0} unchanged line {1}", + "{0} original line {1} modified line {2}", + "+ {0} modified line {1}", + "- {0} original line {1}" + ], + "vs/editor/browser/widget/diffEditor/features/revertButtonsFeature": [ + "Revert Selected Changes", + "Revert Change" + ], + "vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature": [ + "Code moved with changes to line {0}-{1}", + "Code moved with changes from line {0}-{1}", + "Code moved to line {0}-{1}", + "Code moved from line {0}-{1}" + ], + "vs/editor/browser/widget/diffEditor/components/diffEditorEditors": [ + " use {0} to open the accessibility help." ], "vs/workbench/browser/parts/editor/editorPlaceholder": [ "Workspace Trust Required", @@ -29283,42 +32215,38 @@ "The editor could not be opened due to an unexpected error.", "Try Again" ], - "vs/workbench/contrib/comments/browser/commentColors": [ - "Icon color for resolved comments.", - "Icon color for unresolved comments.", - "Color of borders and arrow for resolved comments.", - "Color of borders and arrow for unresolved comments.", - "Color of background for comment ranges.", - "Color of background for currently selected or hovered comment range." + "vs/workbench/browser/parts/paneCompositeBar": [ + "Reset Location", + "Reset Location" ], - "vs/base/browser/ui/menu/menubar": [ - "Application Menu", - "More" + "vs/workbench/browser/parts/compositePart": [ + "{0} actions", + "Views and More Actions...", + "{0} ({1})" ], - "vs/workbench/browser/parts/editor/multiEditorTabsControl": [ - "Tab actions" + "vs/workbench/browser/parts/editor/editorPanes": [ + "Unable to open '{0}'", + "&&OK" ], - "vs/workbench/browser/parts/editor/breadcrumbsControl": [ - "Icon for the separator in the breadcrumbs.", - "Whether the editor can show breadcrumbs", - "Whether breadcrumbs are currently visible", - "Whether breadcrumbs have focus", - "no elements", - "Toggle Breadcrumbs", - "Toggle &&Breadcrumbs", - "Breadcrumbs", - "&&Breadcrumbs", - "Focus and Select Breadcrumbs", - "Focus Breadcrumbs" + "vs/workbench/browser/parts/editor/editorGroupWatermark": [ + "Foreground color for the labels in the editor watermark.", + "Show All Commands", + "Go to File", + "Open File", + "Open Folder", + "Open File or Folder", + "Open Recent", + "New Untitled Text File", + "Find in Files", + "Toggle Terminal", + "Start Debugging", + "Toggle Full Screen", + "Show Settings" ], - "vs/platform/quickinput/browser/quickInput": [ - "Back", - "Press 'Enter' to confirm your input or 'Escape' to cancel", - "{0}/{1}", - "Type to narrow down results.", - "{0} (Press 'Enter' to confirm or 'Escape' to cancel)" + "vs/platform/quickinput/browser/quickInputUtils": [ + "Click to execute command '{0}'" ], - "vs/platform/quickinput/browser/quickInputList": [ + "vs/platform/quickinput/browser/quickInputTree": [ "Quick Input" ], "vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators": [ @@ -29404,18 +32332,37 @@ "Item", "Value" ], + "vs/workbench/contrib/chat/browser/chatAgentHover": [ + "View in Marketplace" + ], + "vs/workbench/contrib/inlineChat/browser/inlineChatWidget": [ + "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", + "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information.", + "Inline Chat Input", + "Original", + "Modified" + ], "vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer": [ "Execution Order" ], - "vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll": [ - "Toggle Notebook Sticky Scroll", - "&&Toggle Notebook Sticky Scroll", - "Notebook Sticky Scroll", - "&&Notebook Sticky Scroll" + "vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider": [ + "Notebook\nUse {0} for accessibility help", + "Notebook\nRun the Open Accessibility Help command for more information", + "Notebook" ], "vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager": [ "Saving working copies" ], + "vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory": [ + "empty cell" + ], + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource": [ + "Display limit reached" + ], + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree": [ + "Notebook Variables", + "Variable {0}, value {1}" + ], "vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget": [ "Find", "Find", @@ -29434,16 +32381,8 @@ "Code Cell Source", "Code Cell Output" ], - "vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory": [ - "empty cell" - ], - "vs/platform/actions/browser/buttonbar": [ - "{0} ({1})" - ], - "vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget": [ - "Exited {0} mode" - ], "vs/workbench/contrib/terminal/browser/xterm/decorationAddon": [ + "Toggle Visibility", "Rerun Command", "Do you want to run the command: {0}", "Yes", @@ -29454,26 +32393,11 @@ "Copy Output as HTML", "Run Recent Command", "Go To Recent Directory", - "Configure Command Decorations", "Learn About Shell Integration", "Toggle visibility", - "Toggle visibility", "Gutter command decorations", "Overview ruler command decorations" ], - "vs/workbench/contrib/debug/common/debugger": [ - "Cannot find debug adapter for type '{0}'.", - "Use IntelliSense to learn about possible attributes.", - "Hover to view descriptions of existing attributes.", - "For more information, visit: {0}", - "Type of configuration.", - "The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled.", - "\"node2\" is no longer supported, use \"node\" instead and set the \"protocol\" attribute to \"inspector\".", - "Request type of configuration. Can be \"launch\" or \"attach\".", - "Windows specific launch configuration attributes.", - "OS X specific launch configuration attributes.", - "Linux specific launch configuration attributes." - ], "vs/workbench/contrib/debug/common/debugSchemas": [ "Contributes debug adapters.", "Unique identifier for this debug adapter.", @@ -29515,11 +32439,23 @@ "Name of folder in which the compound is located.", "Names of configurations that will be started as part of this compound.", "Controls whether manually terminating one session will stop all of the compound sessions.", - "Task to run before any of the compound configurations start." + "Task to run before any of the compound configurations start.", + "Name", + "Type", + "Debuggers" ], - "vs/workbench/contrib/mergeEditor/browser/mergeMarkers/mergeMarkersController": [ - "1 Conflicting Line", - "{0} Conflicting Lines" + "vs/workbench/contrib/debug/common/debugger": [ + "Cannot find debug adapter for type '{0}'.", + "Use IntelliSense to learn about possible attributes.", + "Hover to view descriptions of existing attributes.", + "For more information, visit: {0}", + "Type of configuration.", + "The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled.", + "\"node2\" is no longer supported, use \"node\" instead and set the \"protocol\" attribute to \"inspector\".", + "Request type of configuration. Can be \"launch\" or \"attach\".", + "Windows specific launch configuration attributes.", + "OS X specific launch configuration attributes.", + "Linux specific launch configuration attributes." ], "vs/workbench/contrib/debug/browser/rawDebugSession": [ "No debug adapter, can not start debug session.", @@ -29528,6 +32464,15 @@ "No debugger available found. Can not send '{0}'.", "More Info" ], + "vs/workbench/contrib/comments/browser/commentThreadWidget": [ + "Comment", + "{0}, use ({1}) for accessibility help", + "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding." + ], + "vs/workbench/contrib/mergeEditor/browser/mergeMarkers/mergeMarkersController": [ + "1 Conflicting Line", + "{0} Conflicting Lines" + ], "vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel": [ "Set Input Handled", "Undo Mark As Handled" @@ -29555,40 +32500,13 @@ "Reset to base", "Reset this conflict to the common ancestor of both the right and left changes." ], - "vs/workbench/contrib/comments/browser/commentGlyphWidget": [ - "Editor gutter decoration color for commenting ranges. This color should be opaque.", - "Editor overview ruler decoration color for resolved comments. This color should be opaque.", - "Editor overview ruler decoration color for unresolved comments. This color should be opaque.", - "Editor gutter decoration color for commenting glyphs.", - "Editor gutter decoration color for commenting glyphs for unresolved comment threads." - ], - "vs/workbench/contrib/terminal/browser/terminalConfigHelper": [ - "The '{0}' extension is recommended for opening a terminal in WSL.", - "Install" - ], - "vs/workbench/contrib/customEditor/common/extensionPoint": [ - "Contributed custom editors.", - "Identifier for the custom editor. This must be unique across all custom editors, so we recommend including your extension id as part of `viewType`. The `viewType` is used when registering custom editors with `vscode.registerCustomEditorProvider` and in the `onCustomEditor:${id}` [activation event](https://code.visualstudio.com/api/references/activation-events).", - "Human readable name of the custom editor. This is displayed to users when selecting which editor to use.", - "Set of globs that the custom editor is enabled for.", - "Glob that the custom editor is enabled for.", - "Controls if the custom editor is enabled automatically when the user opens a file. This may be overridden by users using the `workbench.editorAssociations` setting.", - "The editor is automatically used when the user opens a resource, provided that no other default custom editors are registered for that resource.", - "The editor is not automatically used when the user opens a resource, but a user can switch to the editor using the `Reopen With` command." - ], "vs/workbench/contrib/terminal/browser/terminalInstance": [ - "Task", - "Local", "Terminal input", "Use the accessible buffer {0} to manually review output", "Use the Terminal: Focus Accessible Buffer command to manually review output", "Bell", "Some keybindings don't go to the terminal by default and are handled by {0} instead.", "Configure Terminal Settings", - "Preview:", - "Are you sure you want to paste {0} lines of text into the terminal?", - "&&Paste", - "Do not ask me again", "Lost connection to process", "Cannot launch a terminal process in an untrusted workspace", "Cannot launch a terminal process in an untrusted workspace with cwd {0} and userHome {1}", @@ -29603,7 +32521,6 @@ "Set Fixed Dimensions: Column", "Set Fixed Dimensions: Row", "Terminal {0} environment is stale, run the 'Show Environment Information' command for more information", - "Select an icon for the terminal", "Select a color for the terminal", "The terminal process \"{0}\" failed to launch (exit code: {1}).", "The terminal process failed to launch (exit code: {0}).", @@ -29611,6 +32528,20 @@ "The terminal process terminated with exit code: {0}.", "The terminal process failed to launch: {0}." ], + "vs/workbench/contrib/customEditor/common/extensionPoint": [ + "Contributed custom editors.", + "Identifier for the custom editor. This must be unique across all custom editors, so we recommend including your extension id as part of `viewType`. The `viewType` is used when registering custom editors with `vscode.registerCustomEditorProvider` and in the `onCustomEditor:${id}` [activation event](https://code.visualstudio.com/api/references/activation-events).", + "Human readable name of the custom editor. This is displayed to users when selecting which editor to use.", + "Set of globs that the custom editor is enabled for.", + "Glob that the custom editor is enabled for.", + "Controls if the custom editor is enabled automatically when the user opens a file. This may be overridden by users using the `workbench.editorAssociations` setting.", + "The editor is automatically used when the user opens a resource, provided that no other default custom editors are registered for that resource.", + "The editor is not automatically used when the user opens a resource, but a user can switch to the editor using the `Reopen With` command.", + "View Type", + "Priority", + "Filename Pattern", + "Custom Editors" + ], "vs/workbench/contrib/terminal/browser/terminalProfileQuickpick": [ "Select the terminal profile to create", "Select your default terminal profile", @@ -29638,9 +32569,27 @@ "Open folder in new window", "Follow link" ], + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget": [ + "Ask how to do something in the terminal", + "AI-generated commands may be incorrect" + ], + "vs/workbench/contrib/terminal/browser/xterm/decorationStyles": [ + "Show Command Actions", + "Command executed {0}, took {1} and failed", + "Command executed {0}, took {1} and failed (Exit Code {2})", + "Command executed {0} and took {1}", + "Command executed {0} and failed", + "Command executed {0} and failed (Exit Code {1})", + "Command executed {0}" + ], + "vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay": [ + "Navigate to Command", + "{0} ({1})", + "{0} ({1})" + ], "vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget": [ "Find", - "Find (⇅ for history)", + "Find", "Previous Match", "Next Match", "Close", @@ -29650,18 +32599,37 @@ "{0} found for '{1}'", "Border color of the sash border." ], - "vs/workbench/contrib/terminal/browser/xterm/decorationStyles": [ - "Show Command Actions", - "Command executed {0} and failed", - "Command executed {0} and failed (Exit Code {1})", - "Command executed {0}" + "vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu": [ + "Undo", + "Redo", + "Cut", + "Copy", + "Paste", + "Select All" + ], + "vs/workbench/services/suggest/browser/simpleSuggestWidget": [ + "Suggest", + "{0}{1}, {2}", + "{0}{1}", + "{0}, {1}", + "{0}, docs: {1}" + ], + "vs/workbench/contrib/markdown/browser/markdownSettingRenderer": [ + "View in Settings", + "View \"{0}: {1}\" in Settings", + "Restore value of \"{0}: {1}\"", + "Enable \"{0}: {1}\"", + "Disable \"{0}: {1}\"", + "Set \"{0}: {1}\" to \"{2}\"", + "Set \"{0}: {1}\" to {2}", + "View or change setting", + "Copy Setting ID", + "Copy Setting ID" ], - "vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker": [ - "Dark Modern", - "Light Modern", - "Dark High Contrast", - "Light High Contrast", - "See More Themes..." + "vs/workbench/contrib/welcomeGettingStarted/common/media/notebookProfile": [ + "Default", + "Jupyter", + "Colab" ], "vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView": [ "Please go through each entry and merge to resolve conflicts.", @@ -29673,26 +32641,61 @@ "Theirs", "Yours" ], - "vs/workbench/contrib/welcomeGettingStarted/common/media/notebookProfile": [ - "Default", - "Jupyter", - "Colab" + "vs/platform/languagePacks/common/localizedStrings": [ + "open", + "close", + "find" ], "vs/workbench/browser/parts/notifications/notificationsViewer": [ "Click to execute command '{0}'", "Notification Actions", + "Turn On Notifications from '{0}'", + "Turn Off Notifications from '{0}'", "Source: {0}" ], - "vs/platform/languagePacks/common/localizedStrings": [ - "open", - "close", - "find" + "vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker": [ + "Dark Modern", + "Light Modern", + "Dark High Contrast", + "Light High Contrast", + "See More Themes..." + ], + "vs/base/browser/ui/menu/menubar": [ + "Application Menu", + "More" + ], + "vs/workbench/browser/parts/compositeBarActions": [ + "{0} ({1})", + "{0} - {1}", + "Additional Views", + "{0} ({1})", + "Manage Extension", + "Hide '{0}'", + "Keep '{0}'", + "Hide Badge", + "Show Badge", + "Toggle View Pinned", + "Toggle View Badge" ], "vs/editor/common/viewLayout/viewLineRenderer": [ "Show more ({0})", "{0} chars" ], - "vs/editor/browser/widget/diffEditor/inlineDiffDeletedCodeMargin": [ + "vs/platform/actionWidget/browser/actionList": [ + "{0} to Apply, {1} to Preview", + "{0} to Apply", + "{0}, Disabled Reason: {1}", + "Action Widget" + ], + "vs/editor/contrib/gotoSymbol/browser/peek/referencesTree": [ + "{0} references", + "{0} reference", + "References" + ], + "vs/workbench/browser/parts/compositeBar": [ + "Active View Switcher" + ], + "vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/inlineDiffDeletedCodeMargin": [ "Copy deleted lines", "Copy deleted line", "Copy changed lines", @@ -29701,25 +32704,89 @@ "Copy changed line ({0})", "Revert this change" ], - "vs/editor/browser/widget/diffEditor/decorations": [ - "Line decoration for inserts in the diff editor.", - "Line decoration for removals in the diff editor.", - "Click to revert change" + "vs/workbench/browser/parts/editor/breadcrumbsControl": [ + "Icon for the separator in the breadcrumbs.", + "Whether the editor can show breadcrumbs", + "Whether breadcrumbs are currently visible", + "Whether breadcrumbs have focus", + "no elements", + "Toggle &&Breadcrumbs", + "Toggle Breadcrumbs", + "Toggle &&Breadcrumbs", + "Toggle Breadcrumbs", + "Focus and Select Breadcrumbs", + "Focus Breadcrumbs" ], - "vs/platform/actionWidget/browser/actionList": [ - "{0} to apply, {1} to preview", - "{0} to apply", - "{0}, Disabled Reason: {1}", - "Action Widget" + "vs/workbench/browser/parts/editor/multiEditorTabsControl": [ + "Tab actions" ], - "vs/editor/contrib/gotoSymbol/browser/peek/referencesTree": [ - "{0} references", - "{0} reference", - "References" + "vs/platform/actions/browser/buttonbar": [ + "{0} ({1})" ], - "vs/workbench/browser/parts/editor/editorTabsControl": [ - "Editor actions", - "{0} (+{1})" + "vs/workbench/contrib/notebook/browser/diff/diffElementOutputs": [ + "Choose a different output mimetype, available mimetypes: {0}", + "Cell has no output", + "No renderer could be found for output. It has the following mimetypes: {0}", + "Currently Active", + "Select mimetype to render for current output. Rich mimetypes are available only when the notebook is trusted", + "Select mimetype to render for current output", + "built-in" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions": [ + "Controls the display of line numbers in the cell editor.", + "Notebook Line Numbers", + "Show Cell Line Numbers", + "Toggle Notebook Line Numbers" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/codeCell": [ + "Double-click to expand cell input ({0})", + "Expand Cell Input ({0})" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar": [ + "More..." + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint": [ + "1 cell hidden", + "{0} cells hidden" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellOutput": [ + "Outputs are collapsed", + "Double-click to expand cell output ({0})", + "Expand Cell Output (${0})" + ], + "vs/workbench/contrib/notebook/browser/view/cellParts/markupCell": [ + "Double-click to expand cell input ({0})", + "Expand Cell Input ({0})" + ], + "vs/workbench/contrib/comments/browser/commentThreadHeader": [ + "Icon to collapse a review comment.", + "Collapse", + "Start discussion" + ], + "vs/workbench/contrib/comments/browser/commentThreadBody": [ + "Comment thread with {0} comments on lines {1} through {2}. {3}.", + "Comment thread with {0} comments on the entire document. {1}.", + "Comment thread with {0} comments. {1}." + ], + "vs/workbench/contrib/terminal/browser/terminalProcessManager": [ + "Could not kill process listening on port {0}, command exited with error {1}", + "Restarting the terminal because the connection to the shell process was lost..." + ], + "vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick": [ + "Remove from Command History", + "View Command Output", + "Select a command to run (hold Option-key to edit the command)", + "Select a command to run (hold Alt-key to edit the command)", + "{0} history", + "Select a directory to go to (hold Option-key to edit the command)", + "Select a directory to go to (hold Alt-key to edit the command)" + ], + "vs/workbench/contrib/terminal/common/terminalClipboard": [ + "Preview:", + "Are you sure you want to paste {0} lines of text into the terminal?", + "&&Paste", + "Paste as &&one line", + "Do not ask me again" ], "vs/workbench/browser/parts/editor/breadcrumbs": [ "Breadcrumb Navigation", @@ -29767,69 +32834,6 @@ "vs/workbench/browser/parts/editor/breadcrumbsPicker": [ "Breadcrumbs" ], - "vs/platform/quickinput/browser/quickInputUtils": [ - "Click to execute command '{0}'" - ], - "vs/workbench/contrib/notebook/browser/diff/diffElementOutputs": [ - "Choose a different output mimetype, available mimetypes: {0}", - "Cell has no output", - "No renderer could be found for output. It has the following mimetypes: {0}", - "Currently Active", - "Select mimetype to render for current output. Rich mimetypes are available only when the notebook is trusted", - "Select mimetype to render for current output", - "built-in" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions": [ - "Controls the display of line numbers in the cell editor.", - "Toggle Notebook Line Numbers", - "Notebook Line Numbers", - "Show Cell Line Numbers" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/codeCell": [ - "Double-click to expand cell input ({0})", - "Expand Cell Input ({0})" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar": [ - "More..." - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint": [ - "1 cell hidden", - "{0} cells hidden" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/markupCell": [ - "Double-click to expand cell input ({0})", - "Expand Cell Input ({0})" - ], - "vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellOutput": [ - "Outputs are collapsed", - "Double-click to expand cell output ({0})", - "Expand Cell Output (${0})" - ], - "vs/workbench/services/suggest/browser/simpleSuggestWidget": [ - "Suggest", - "{0}{1}, {2}", - "{0}{1}", - "{0}, {1}", - "{0}, docs: {1}" - ], - "vs/workbench/contrib/comments/browser/commentThreadWidget": [ - "Comment", - "{0}, use ({1}) for accessibility help", - "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding." - ], - "vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick": [ - "Remove from Command History", - "View Command Output", - "Select a command to run (hold Option-key to edit the command)", - "Select a command to run (hold Alt-key to edit the command)", - "{0} history", - "Select a directory to go to (hold Option-key to edit the command)", - "Select a directory to go to (hold Alt-key to edit the command)" - ], - "vs/workbench/contrib/terminal/browser/terminalProcessManager": [ - "Could not kill process listening on port {0}, command exited with error {1}", - "Restarting the terminal because the connection to the shell process was lost..." - ], "vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput": [ "Cell has no output", "No renderer could be found for output. It has the following mimetypes: {0}", @@ -29840,22 +32844,21 @@ "Select mimetype to render for current output", "renderer not available" ], + "vs/workbench/contrib/comments/browser/commentNode": [ + "Toggle Reaction", + "Toggling the comment reaction failed: {0}.", + "Toggling the comment reaction failed", + "Deleting the comment reaction failed: {0}.", + "Deleting the comment reaction failed", + "Deleting the comment reaction failed: {0}.", + "Deleting the comment reaction failed" + ], "vs/workbench/contrib/notebook/browser/view/cellParts/codeCellExecutionIcon": [ "Success", - "Failed", + "Failure", "Pending", "Executing" ], - "vs/workbench/contrib/comments/browser/commentThreadBody": [ - "Comment thread with {0} comments on lines {1} through {2}. {3}.", - "Comment thread with {0} comments on the entire document. {1}.", - "Comment thread with {0} comments. {1}." - ], - "vs/workbench/contrib/comments/browser/commentThreadHeader": [ - "Icon to collapse a review comment.", - "Collapse", - "Start discussion" - ], "vs/workbench/contrib/terminal/browser/environmentVariableInfo": [ "The following extensions want to relaunch the terminal to contribute to its environment:", "Relaunch terminal", @@ -29863,21 +32866,14 @@ "Show environment contributions", "workspace" ], - "vs/workbench/contrib/comments/browser/commentNode": [ - "Toggle Reaction", - "Toggling the comment reaction failed: {0}.", - "Toggling the comment reaction failed", - "Deleting the comment reaction failed: {0}.", - "Deleting the comment reaction failed", - "Deleting the comment reaction failed: {0}.", - "Deleting the comment reaction failed" - ], "vs/workbench/contrib/comments/browser/reactionsAction": [ "Pick Reactions...", "Toggle reaction, ", "{0}{1} reaction", "{0}1 reaction with {1}", - "{0}{1} reactions with {2}" + "{0}{1} reactions with {2}", + "{0}{1} reacted with {2}", + "{0}{1} and {2} more reacted with {3}" ] }, "bundles": { @@ -29890,7 +32886,6 @@ "vs/base/browser/ui/findinput/findInputToggles", "vs/base/browser/ui/findinput/replaceInput", "vs/base/browser/ui/hover/hoverWidget", - "vs/base/browser/ui/iconLabel/iconLabelHover", "vs/base/browser/ui/icons/iconSelectBox", "vs/base/browser/ui/inputbox/inputBox", "vs/base/browser/ui/keybindingLabel/keybindingLabel", @@ -29909,15 +32904,19 @@ "vs/editor/browser/controller/textAreaHandler", "vs/editor/browser/coreCommands", "vs/editor/browser/editorExtensions", - "vs/editor/browser/widget/codeEditorWidget", - "vs/editor/browser/widget/diffEditor/accessibleDiffViewer", - "vs/editor/browser/widget/diffEditor/colors", - "vs/editor/browser/widget/diffEditor/decorations", + "vs/editor/browser/services/hoverService/hoverWidget", + "vs/editor/browser/services/hoverService/updatableHoverWidget", + "vs/editor/browser/widget/codeEditor/codeEditorWidget", + "vs/editor/browser/widget/diffEditor/commands", + "vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer", + "vs/editor/browser/widget/diffEditor/components/diffEditorEditors", + "vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/inlineDiffDeletedCodeMargin", "vs/editor/browser/widget/diffEditor/diffEditor.contribution", - "vs/editor/browser/widget/diffEditor/diffEditorEditors", - "vs/editor/browser/widget/diffEditor/hideUnchangedRegionsFeature", - "vs/editor/browser/widget/diffEditor/inlineDiffDeletedCodeMargin", - "vs/editor/browser/widget/diffEditor/movedBlocksLines", + "vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature", + "vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature", + "vs/editor/browser/widget/diffEditor/features/revertButtonsFeature", + "vs/editor/browser/widget/diffEditor/registrations.contribution", + "vs/editor/browser/widget/multiDiffEditor/colors", "vs/editor/common/config/editorConfigurationSchema", "vs/editor/common/config/editorOptions", "vs/editor/common/core/editorColorRegistry", @@ -29955,7 +32954,6 @@ "vs/editor/contrib/folding/browser/folding", "vs/editor/contrib/folding/browser/foldingDecorations", "vs/editor/contrib/fontZoom/browser/fontZoom", - "vs/editor/contrib/format/browser/format", "vs/editor/contrib/format/browser/formatActions", "vs/editor/contrib/gotoError/browser/gotoError", "vs/editor/contrib/gotoError/browser/gotoErrorWidget", @@ -29966,7 +32964,7 @@ "vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget", "vs/editor/contrib/gotoSymbol/browser/referencesModel", "vs/editor/contrib/gotoSymbol/browser/symbolNavigation", - "vs/editor/contrib/hover/browser/hover", + "vs/editor/contrib/hover/browser/hoverActions", "vs/editor/contrib/hover/browser/markdownHoverParticipant", "vs/editor/contrib/hover/browser/markerHoverParticipant", "vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace", @@ -29990,7 +32988,7 @@ "vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess", "vs/editor/contrib/readOnlyMessage/browser/contribution", "vs/editor/contrib/rename/browser/rename", - "vs/editor/contrib/rename/browser/renameInputField", + "vs/editor/contrib/rename/browser/renameWidget", "vs/editor/contrib/smartSelect/browser/smartSelect", "vs/editor/contrib/snippet/browser/snippetController2", "vs/editor/contrib/snippet/browser/snippetVariables", @@ -30009,6 +33007,7 @@ "vs/editor/contrib/wordHighlighter/browser/highlightDecorations", "vs/editor/contrib/wordHighlighter/browser/wordHighlighter", "vs/editor/contrib/wordOperations/browser/wordOperations", + "vs/platform/accessibilitySignal/browser/accessibilitySignalService", "vs/platform/action/common/actionCommonCategories", "vs/platform/actionWidget/browser/actionList", "vs/platform/actionWidget/browser/actionWidget", @@ -30017,7 +33016,6 @@ "vs/platform/actions/browser/toolbar", "vs/platform/actions/common/menuResetAction", "vs/platform/actions/common/menuService", - "vs/platform/audioCues/browser/audioCueService", "vs/platform/configuration/common/configurationRegistry", "vs/platform/contextkey/browser/contextKeyService", "vs/platform/contextkey/common/contextkey", @@ -30040,12 +33038,13 @@ "vs/platform/languagePacks/common/languagePacks", "vs/platform/languagePacks/common/localizedStrings", "vs/platform/list/browser/listService", + "vs/platform/log/common/log", "vs/platform/markers/common/markers", "vs/platform/quickinput/browser/commandsQuickAccess", "vs/platform/quickinput/browser/helpQuickAccess", "vs/platform/quickinput/browser/quickInput", "vs/platform/quickinput/browser/quickInputController", - "vs/platform/quickinput/browser/quickInputList", + "vs/platform/quickinput/browser/quickInputTree", "vs/platform/quickinput/browser/quickInputUtils", "vs/platform/quickinput/browser/quickPickPin", "vs/platform/remoteTunnel/common/remoteTunnel", @@ -30054,7 +33053,16 @@ "vs/platform/terminal/common/terminalLogService", "vs/platform/terminal/common/terminalPlatformConfiguration", "vs/platform/terminal/common/terminalProfiles", - "vs/platform/theme/common/colorRegistry", + "vs/platform/theme/common/colors/baseColors", + "vs/platform/theme/common/colors/chartsColors", + "vs/platform/theme/common/colors/editorColors", + "vs/platform/theme/common/colors/inputColors", + "vs/platform/theme/common/colors/listColors", + "vs/platform/theme/common/colors/menuColors", + "vs/platform/theme/common/colors/minimapColors", + "vs/platform/theme/common/colors/miscColors", + "vs/platform/theme/common/colors/quickpickColors", + "vs/platform/theme/common/colors/searchColors", "vs/platform/theme/common/iconRegistry", "vs/platform/theme/common/tokenClassificationRegistry", "vs/platform/undoRedo/common/undoRedoService", @@ -30065,7 +33073,6 @@ "vs/platform/userDataSync/common/userDataSyncLog", "vs/platform/userDataSync/common/userDataSyncMachines", "vs/platform/workspace/common/workspace", - "vs/platform/workspace/common/workspaceTrust", "vs/workbench/api/browser/mainThreadAuthentication", "vs/workbench/api/browser/mainThreadCLICommands", "vs/workbench/api/browser/mainThreadComments", @@ -30073,6 +33080,7 @@ "vs/workbench/api/browser/mainThreadEditSessionIdentityParticipant", "vs/workbench/api/browser/mainThreadExtensionService", "vs/workbench/api/browser/mainThreadFileSystemEventService", + "vs/workbench/api/browser/mainThreadLanguageModels", "vs/workbench/api/browser/mainThreadMessageService", "vs/workbench/api/browser/mainThreadNotebookSaveParticipant", "vs/workbench/api/browser/mainThreadProgress", @@ -30090,6 +33098,7 @@ "vs/workbench/browser/actions/developerActions", "vs/workbench/browser/actions/helpActions", "vs/workbench/browser/actions/layoutActions", + "vs/workbench/browser/actions/listCommands", "vs/workbench/browser/actions/navigationActions", "vs/workbench/browser/actions/quickAccessActions", "vs/workbench/browser/actions/textInputActions", @@ -30097,7 +33106,7 @@ "vs/workbench/browser/actions/workspaceActions", "vs/workbench/browser/actions/workspaceCommands", "vs/workbench/browser/editor", - "vs/workbench/browser/parts/activitybar/activitybarActions", + "vs/workbench/browser/labels", "vs/workbench/browser/parts/activitybar/activitybarPart", "vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions", "vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart", @@ -30111,6 +33120,7 @@ "vs/workbench/browser/parts/editor/breadcrumbs", "vs/workbench/browser/parts/editor/breadcrumbsControl", "vs/workbench/browser/parts/editor/breadcrumbsPicker", + "vs/workbench/browser/parts/editor/diffEditorCommands", "vs/workbench/browser/parts/editor/editor.contribution", "vs/workbench/browser/parts/editor/editorActions", "vs/workbench/browser/parts/editor/editorCommands", @@ -30119,6 +33129,7 @@ "vs/workbench/browser/parts/editor/editorGroupView", "vs/workbench/browser/parts/editor/editorGroupWatermark", "vs/workbench/browser/parts/editor/editorPanes", + "vs/workbench/browser/parts/editor/editorParts", "vs/workbench/browser/parts/editor/editorPlaceholder", "vs/workbench/browser/parts/editor/editorQuickAccess", "vs/workbench/browser/parts/editor/editorStatus", @@ -30128,6 +33139,7 @@ "vs/workbench/browser/parts/editor/textCodeEditor", "vs/workbench/browser/parts/editor/textDiffEditor", "vs/workbench/browser/parts/editor/textEditor", + "vs/workbench/browser/parts/globalCompositeBar", "vs/workbench/browser/parts/notifications/notificationsActions", "vs/workbench/browser/parts/notifications/notificationsAlerts", "vs/workbench/browser/parts/notifications/notificationsCenter", @@ -30136,13 +33148,17 @@ "vs/workbench/browser/parts/notifications/notificationsStatus", "vs/workbench/browser/parts/notifications/notificationsToasts", "vs/workbench/browser/parts/notifications/notificationsViewer", + "vs/workbench/browser/parts/paneCompositeBar", + "vs/workbench/browser/parts/paneCompositePart", "vs/workbench/browser/parts/panel/panelActions", "vs/workbench/browser/parts/panel/panelPart", "vs/workbench/browser/parts/sidebar/sidebarActions", + "vs/workbench/browser/parts/sidebar/sidebarPart", "vs/workbench/browser/parts/statusbar/statusbarActions", "vs/workbench/browser/parts/statusbar/statusbarPart", "vs/workbench/browser/parts/titlebar/commandCenterControl", "vs/workbench/browser/parts/titlebar/menubarControl", + "vs/workbench/browser/parts/titlebar/titlebarActions", "vs/workbench/browser/parts/titlebar/titlebarPart", "vs/workbench/browser/parts/titlebar/windowTitle", "vs/workbench/browser/parts/views/checkbox", @@ -30150,8 +33166,8 @@ "vs/workbench/browser/parts/views/viewFilter", "vs/workbench/browser/parts/views/viewPane", "vs/workbench/browser/parts/views/viewPaneContainer", - "vs/workbench/browser/parts/views/viewsService", "vs/workbench/browser/quickaccess", + "vs/workbench/browser/window", "vs/workbench/browser/workbench", "vs/workbench/browser/workbench.contribution", "vs/workbench/common/configuration", @@ -30163,12 +33179,17 @@ "vs/workbench/common/theme", "vs/workbench/common/views", "vs/workbench/contrib/accessibility/browser/accessibilityConfiguration", - "vs/workbench/contrib/accessibility/browser/accessibilityContributions", "vs/workbench/contrib/accessibility/browser/accessibilityStatus", "vs/workbench/contrib/accessibility/browser/accessibleView", "vs/workbench/contrib/accessibility/browser/accessibleViewActions", - "vs/workbench/contrib/audioCues/browser/audioCues.contribution", - "vs/workbench/contrib/audioCues/browser/commands", + "vs/workbench/contrib/accessibility/browser/accessibleViewContributions", + "vs/workbench/contrib/accessibility/browser/audioCueConfiguration", + "vs/workbench/contrib/accessibilitySignals/browser/commands", + "vs/workbench/contrib/accessibilitySignals/browser/openDiffEditorAnnouncement", + "vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution", + "vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction", + "vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction", + "vs/workbench/contrib/authentication/browser/authentication.contribution", "vs/workbench/contrib/bulkEdit/browser/bulkEditService", "vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution", "vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane", @@ -30188,30 +33209,33 @@ "vs/workbench/contrib/chat/browser/actions/chatMoveActions", "vs/workbench/contrib/chat/browser/actions/chatQuickInputActions", "vs/workbench/contrib/chat/browser/actions/chatTitleActions", + "vs/workbench/contrib/chat/browser/chat", "vs/workbench/contrib/chat/browser/chat.contribution", - "vs/workbench/contrib/chat/browser/chatContributionServiceImpl", + "vs/workbench/contrib/chat/browser/chatAccessibilityProvider", + "vs/workbench/contrib/chat/browser/chatAgentHover", "vs/workbench/contrib/chat/browser/chatEditorInput", + "vs/workbench/contrib/chat/browser/chatFollowups", "vs/workbench/contrib/chat/browser/chatInputPart", "vs/workbench/contrib/chat/browser/chatListRenderer", - "vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget", + "vs/workbench/contrib/chat/browser/chatParticipantContributions", + "vs/workbench/contrib/chat/browser/codeBlockPart", + "vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables", "vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib", - "vs/workbench/contrib/chat/common/chatAgents", "vs/workbench/contrib/chat/common/chatColors", "vs/workbench/contrib/chat/common/chatContextKeys", "vs/workbench/contrib/chat/common/chatServiceImpl", - "vs/workbench/contrib/chat/common/chatSlashCommands", - "vs/workbench/contrib/chat/common/chatViewModel", + "vs/workbench/contrib/chat/common/languageModelStats", "vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions", "vs/workbench/contrib/codeActions/browser/codeActionsContribution", "vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint", "vs/workbench/contrib/codeActions/common/documentationExtensionPoint", "vs/workbench/contrib/codeEditor/browser/accessibility/accessibility", + "vs/workbench/contrib/codeEditor/browser/dictation/editorDictation", "vs/workbench/contrib/codeEditor/browser/diffEditorHelper", "vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint", "vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget", "vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens", "vs/workbench/contrib/codeEditor/browser/inspectKeybindings", - "vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint", "vs/workbench/contrib/codeEditor/browser/largeFileOptimizations", "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline", "vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree", @@ -30224,6 +33248,7 @@ "vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter", "vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace", "vs/workbench/contrib/codeEditor/browser/toggleWordWrap", + "vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint", "vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard", "vs/workbench/contrib/codeEditor/electron-sandbox/startDebugTextMate", "vs/workbench/contrib/commands/common/commands.contribution", @@ -30235,14 +33260,16 @@ "vs/workbench/contrib/comments/browser/commentThreadHeader", "vs/workbench/contrib/comments/browser/commentThreadWidget", "vs/workbench/contrib/comments/browser/comments.contribution", + "vs/workbench/contrib/comments/browser/commentsAccessibility", "vs/workbench/contrib/comments/browser/commentsController", "vs/workbench/contrib/comments/browser/commentsEditorContribution", + "vs/workbench/contrib/comments/browser/commentsModel", "vs/workbench/contrib/comments/browser/commentsTreeViewer", "vs/workbench/contrib/comments/browser/commentsView", "vs/workbench/contrib/comments/browser/commentsViewActions", "vs/workbench/contrib/comments/browser/reactionsAction", "vs/workbench/contrib/comments/common/commentContextKeys", - "vs/workbench/contrib/comments/common/commentModel", + "vs/workbench/contrib/customEditor/browser/customEditorInput", "vs/workbench/contrib/customEditor/common/contributedCustomEditors", "vs/workbench/contrib/customEditor/common/customEditor", "vs/workbench/contrib/customEditor/common/extensionPoint", @@ -30306,6 +33333,7 @@ "vs/workbench/contrib/extensions/browser/exeBasedRecommendations", "vs/workbench/contrib/extensions/browser/extensionEditor", "vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant", + "vs/workbench/contrib/extensions/browser/extensionFeaturesTab", "vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService", "vs/workbench/contrib/extensions/browser/extensions.contribution", "vs/workbench/contrib/extensions/browser/extensionsActions", @@ -30364,11 +33392,15 @@ "vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty", "vs/workbench/contrib/inlineChat/browser/inlineChatActions", "vs/workbench/contrib/inlineChat/browser/inlineChatController", + "vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl", "vs/workbench/contrib/inlineChat/browser/inlineChatStrategies", "vs/workbench/contrib/inlineChat/browser/inlineChatWidget", + "vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget", "vs/workbench/contrib/inlineChat/common/inlineChat", + "vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions", "vs/workbench/contrib/interactive/browser/interactive.contribution", "vs/workbench/contrib/interactive/browser/interactiveEditor", + "vs/workbench/contrib/issue/browser/issueQuickAccess", "vs/workbench/contrib/issue/common/issue.contribution", "vs/workbench/contrib/issue/electron-sandbox/issue.contribution", "vs/workbench/contrib/keybindings/browser/keybindings.contribution", @@ -30386,6 +33418,7 @@ "vs/workbench/contrib/logs/common/logs.contribution", "vs/workbench/contrib/logs/common/logsActions", "vs/workbench/contrib/logs/electron-sandbox/logsActions", + "vs/workbench/contrib/markdown/browser/markdownSettingRenderer", "vs/workbench/contrib/markers/browser/markers.contribution", "vs/workbench/contrib/markers/browser/markersFileDecorations", "vs/workbench/contrib/markers/browser/markersTable", @@ -30408,6 +33441,11 @@ "vs/workbench/contrib/mergeEditor/browser/view/viewModel", "vs/workbench/contrib/mergeEditor/common/mergeEditor", "vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands", + "vs/workbench/contrib/multiDiffEditor/browser/actions", + "vs/workbench/contrib/multiDiffEditor/browser/icons.contribution", + "vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution", + "vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput", + "vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver", "vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands", "vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController", "vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders", @@ -30420,23 +33458,34 @@ "vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted", "vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions", "vs/workbench/contrib/notebook/browser/contrib/navigation/arrow", + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariableCommands", + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariables", + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource", + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree", + "vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView", "vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline", "vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile", "vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants", "vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout", "vs/workbench/contrib/notebook/browser/controller/cellOperations", "vs/workbench/contrib/notebook/browser/controller/cellOutputActions", + "vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions", + "vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext", + "vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController", "vs/workbench/contrib/notebook/browser/controller/coreActions", "vs/workbench/contrib/notebook/browser/controller/editActions", "vs/workbench/contrib/notebook/browser/controller/executeActions", "vs/workbench/contrib/notebook/browser/controller/foldingController", "vs/workbench/contrib/notebook/browser/controller/insertCellActions", "vs/workbench/contrib/notebook/browser/controller/layoutActions", + "vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions", + "vs/workbench/contrib/notebook/browser/controller/sectionActions", "vs/workbench/contrib/notebook/browser/diff/diffElementOutputs", "vs/workbench/contrib/notebook/browser/diff/notebookDiffActions", "vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor", "vs/workbench/contrib/notebook/browser/notebook.contribution", "vs/workbench/contrib/notebook/browser/notebookAccessibility", + "vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider", "vs/workbench/contrib/notebook/browser/notebookEditor", "vs/workbench/contrib/notebook/browser/notebookEditorWidget", "vs/workbench/contrib/notebook/browser/notebookExtensionPoint", @@ -30457,7 +33506,6 @@ "vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView", "vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer", "vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory", - "vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll", "vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy", "vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView", "vs/workbench/contrib/notebook/common/notebookEditorInput", @@ -30493,6 +33541,7 @@ "vs/workbench/contrib/relauncher/browser/relauncher.contribution", "vs/workbench/contrib/remote/browser/explorerViewItems", "vs/workbench/contrib/remote/browser/remote", + "vs/workbench/contrib/remote/browser/remoteConnectionHealth", "vs/workbench/contrib/remote/browser/remoteExplorer", "vs/workbench/contrib/remote/browser/remoteIcons", "vs/workbench/contrib/remote/browser/remoteIndicator", @@ -30508,9 +33557,9 @@ "vs/workbench/contrib/scm/browser/menus", "vs/workbench/contrib/scm/browser/scm.contribution", "vs/workbench/contrib/scm/browser/scmRepositoriesViewPane", - "vs/workbench/contrib/scm/browser/scmSyncViewPane", "vs/workbench/contrib/scm/browser/scmViewPane", "vs/workbench/contrib/scm/browser/scmViewPaneContainer", + "vs/workbench/contrib/scrollLocking/browser/scrollLocking", "vs/workbench/contrib/search/browser/anythingQuickAccess", "vs/workbench/contrib/search/browser/patternInputWidget", "vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess", @@ -30548,6 +33597,8 @@ "vs/workbench/contrib/snippets/browser/snippets.contribution", "vs/workbench/contrib/snippets/browser/snippetsFile", "vs/workbench/contrib/snippets/browser/snippetsService", + "vs/workbench/contrib/speech/browser/speechService", + "vs/workbench/contrib/speech/common/speechService", "vs/workbench/contrib/surveys/browser/ces.contribution", "vs/workbench/contrib/surveys/browser/languageSurveys.contribution", "vs/workbench/contrib/surveys/browser/nps.contribution", @@ -30572,7 +33623,6 @@ "vs/workbench/contrib/terminal/browser/environmentVariableInfo", "vs/workbench/contrib/terminal/browser/terminal.contribution", "vs/workbench/contrib/terminal/browser/terminalActions", - "vs/workbench/contrib/terminal/browser/terminalConfigHelper", "vs/workbench/contrib/terminal/browser/terminalEditorInput", "vs/workbench/contrib/terminal/browser/terminalIcons", "vs/workbench/contrib/terminal/browser/terminalInstance", @@ -30586,10 +33636,12 @@ "vs/workbench/contrib/terminal/browser/terminalTabsList", "vs/workbench/contrib/terminal/browser/terminalTooltip", "vs/workbench/contrib/terminal/browser/terminalView", + "vs/workbench/contrib/terminal/browser/terminalWslRecommendationContribution", "vs/workbench/contrib/terminal/browser/xterm/decorationAddon", "vs/workbench/contrib/terminal/browser/xterm/decorationStyles", "vs/workbench/contrib/terminal/browser/xterm/xtermTerminal", "vs/workbench/contrib/terminal/common/terminal", + "vs/workbench/contrib/terminal/common/terminalClipboard", "vs/workbench/contrib/terminal/common/terminalColorRegistry", "vs/workbench/contrib/terminal/common/terminalConfiguration", "vs/workbench/contrib/terminal/common/terminalContextKey", @@ -30597,9 +33649,14 @@ "vs/workbench/contrib/terminal/electron-sandbox/terminalRemote", "vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution", "vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp", + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChat", + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp", + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions", + "vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget", "vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution", "vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution", "vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution", + "vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu", "vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution", "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkDetectorAdapter", "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager", @@ -30608,7 +33665,14 @@ "vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution", "vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixBuiltinActions", "vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixService", + "vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry", + "vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay", + "vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution", + "vs/workbench/contrib/terminalContrib/zoom/browser/terminal.zoom.contribution", + "vs/workbench/contrib/testing/browser/codeCoverageDecorations", "vs/workbench/contrib/testing/browser/icons", + "vs/workbench/contrib/testing/browser/testCoverageBars", + "vs/workbench/contrib/testing/browser/testCoverageView", "vs/workbench/contrib/testing/browser/testExplorerActions", "vs/workbench/contrib/testing/browser/testing.contribution", "vs/workbench/contrib/testing/browser/testingConfigurationUi", @@ -30650,7 +33714,6 @@ "vs/workbench/contrib/webviewPanel/browser/webviewCommands", "vs/workbench/contrib/webviewPanel/browser/webviewEditor", "vs/workbench/contrib/webviewPanel/browser/webviewPanel.contribution", - "vs/workbench/contrib/welcomeGettingStarted/browser/featuredExtensionService", "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted", "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution", "vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors", @@ -30682,7 +33745,10 @@ "vs/workbench/electron-sandbox/window", "vs/workbench/services/actions/common/menusExtensionPoint", "vs/workbench/services/assignment/common/assignmentService", + "vs/workbench/services/authentication/browser/authenticationExtensionsService", "vs/workbench/services/authentication/browser/authenticationService", + "vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService", + "vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService", "vs/workbench/services/configuration/browser/configurationService", "vs/workbench/services/configuration/common/configurationEditing", "vs/workbench/services/configuration/common/jsonEditingService", @@ -30697,6 +33763,8 @@ "vs/workbench/services/editor/common/editorResolverService", "vs/workbench/services/extensionManagement/browser/extensionBisect", "vs/workbench/services/extensionManagement/browser/extensionEnablementService", + "vs/workbench/services/extensionManagement/common/extensionFeaturesManagemetService", + "vs/workbench/services/extensionManagement/common/extensionManagement", "vs/workbench/services/extensionManagement/common/extensionManagementService", "vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService", "vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService", @@ -30709,9 +33777,9 @@ "vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner", "vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost", "vs/workbench/services/extensions/electron-sandbox/nativeExtensionService", + "vs/workbench/services/files/electron-sandbox/diskFileSystemProvider", "vs/workbench/services/filesConfiguration/common/filesConfigurationService", "vs/workbench/services/history/browser/historyService", - "vs/workbench/services/hover/browser/hoverWidget", "vs/workbench/services/integrity/electron-sandbox/integrityService", "vs/workbench/services/issue/browser/issueTroubleshoot", "vs/workbench/services/keybinding/browser/keybindingService", @@ -30728,6 +33796,7 @@ "vs/workbench/services/preferences/common/preferencesModels", "vs/workbench/services/preferences/common/preferencesValidation", "vs/workbench/services/progress/browser/progressService", + "vs/workbench/services/remote/common/remoteExplorerService", "vs/workbench/services/remote/common/tunnelModel", "vs/workbench/services/remote/electron-sandbox/remoteAgentService", "vs/workbench/services/search/common/queryBuilder", @@ -30752,6 +33821,7 @@ "vs/workbench/services/themes/common/themeConfiguration", "vs/workbench/services/themes/common/themeExtensionPoints", "vs/workbench/services/themes/common/tokenClassificationExtensionPoint", + "vs/workbench/services/themes/electron-sandbox/themes.contribution", "vs/workbench/services/userDataProfile/browser/extensionsResource", "vs/workbench/services/userDataProfile/browser/globalStateResource", "vs/workbench/services/userDataProfile/browser/keybindingsResource", @@ -30765,8 +33835,8 @@ "vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService", "vs/workbench/services/userDataSync/common/userDataSync", "vs/workbench/services/views/browser/viewDescriptorService", + "vs/workbench/services/views/browser/viewsService", "vs/workbench/services/views/common/viewContainerModel", - "vs/workbench/services/voiceRecognition/electron-sandbox/workbenchVoiceRecognitionService", "vs/workbench/services/workingCopy/common/fileWorkingCopyManager", "vs/workbench/services/workingCopy/common/storedFileWorkingCopy", "vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager", @@ -30797,29 +33867,34 @@ "vs/platform/extensionManagement/common/extensionManagement", "vs/platform/extensions/common/extensionValidator", "vs/platform/files/common/files", + "vs/platform/log/common/log", "vs/platform/markers/common/markers", + "vs/platform/request/common/request", "vs/platform/theme/common/iconRegistry", + "vs/platform/userDataProfile/common/userDataProfile", "vs/platform/workspace/common/workspace", - "vs/workbench/api/common/extHostChat", "vs/workbench/api/common/extHostDiagnostics", "vs/workbench/api/common/extHostExtensionService", "vs/workbench/api/common/extHostLanguageFeatures", + "vs/workbench/api/common/extHostLanguageModels", "vs/workbench/api/common/extHostLogService", "vs/workbench/api/common/extHostNotebook", - "vs/workbench/api/common/extHostProgress", "vs/workbench/api/common/extHostStatusBar", "vs/workbench/api/common/extHostTelemetry", "vs/workbench/api/common/extHostTerminalService", "vs/workbench/api/common/extHostTreeViews", "vs/workbench/api/common/extHostTunnelService", "vs/workbench/api/common/extHostWorkspace", + "vs/workbench/common/configuration", "vs/workbench/common/editor", "vs/workbench/common/views", + "vs/workbench/contrib/chat/common/chatContextKeys", "vs/workbench/contrib/debug/common/abstractDebugAdapter", "vs/workbench/contrib/debug/common/debug", "vs/workbench/contrib/tasks/common/taskDefinitionRegistry", "vs/workbench/contrib/tasks/common/tasks", "vs/workbench/services/configurationResolver/common/variableResolver", + "vs/workbench/services/editor/common/editorResolverService", "vs/workbench/services/extensions/common/extensionsRegistry", "vs/workbench/services/search/common/queryBuilder" ], @@ -30843,6 +33918,7 @@ "vs/platform/contextkey/common/scanner", "vs/platform/environment/node/argv", "vs/platform/files/common/files", + "vs/platform/log/common/log", "vs/platform/terminal/node/ptyHostMain", "vs/platform/terminal/node/ptyService", "vs/platform/terminal/node/terminalProcess" @@ -30864,17 +33940,18 @@ "vs/platform/files/common/files", "vs/platform/files/common/io", "vs/platform/files/node/diskFileSystemProvider", + "vs/platform/log/common/log", "vs/platform/markers/common/markers", + "vs/platform/request/common/request", "vs/platform/theme/common/iconRegistry", "vs/platform/userDataProfile/common/userDataProfile", "vs/platform/workspace/common/workspace", - "vs/workbench/api/common/extHostChat", "vs/workbench/api/common/extHostDiagnostics", "vs/workbench/api/common/extHostExtensionService", "vs/workbench/api/common/extHostLanguageFeatures", + "vs/workbench/api/common/extHostLanguageModels", "vs/workbench/api/common/extHostLogService", "vs/workbench/api/common/extHostNotebook", - "vs/workbench/api/common/extHostProgress", "vs/workbench/api/common/extHostStatusBar", "vs/workbench/api/common/extHostTelemetry", "vs/workbench/api/common/extHostTerminalService", @@ -30882,14 +33959,17 @@ "vs/workbench/api/common/extHostTunnelService", "vs/workbench/api/common/extHostWorkspace", "vs/workbench/api/node/extHostDebugService", + "vs/workbench/common/configuration", "vs/workbench/common/editor", "vs/workbench/common/views", + "vs/workbench/contrib/chat/common/chatContextKeys", "vs/workbench/contrib/debug/common/abstractDebugAdapter", "vs/workbench/contrib/debug/common/debug", "vs/workbench/contrib/debug/node/debugAdapter", "vs/workbench/contrib/tasks/common/taskDefinitionRegistry", "vs/workbench/contrib/tasks/common/tasks", "vs/workbench/services/configurationResolver/common/variableResolver", + "vs/workbench/services/editor/common/editorResolverService", "vs/workbench/services/extensions/common/extensionsRegistry", "vs/workbench/services/remote/common/tunnelModel", "vs/workbench/services/search/common/queryBuilder" @@ -30919,6 +33999,7 @@ "vs/platform/files/electron-main/diskFileSystemProviderServer", "vs/platform/files/node/diskFileSystemProvider", "vs/platform/issue/electron-main/issueMainService", + "vs/platform/log/common/log", "vs/platform/menubar/electron-main/menubar", "vs/platform/native/electron-main/nativeHostMainService", "vs/platform/request/common/request", @@ -30961,6 +34042,7 @@ "vs/platform/files/common/io", "vs/platform/files/node/diskFileSystemProvider", "vs/platform/languagePacks/common/languagePacks", + "vs/platform/log/common/log", "vs/platform/request/common/request", "vs/platform/shell/node/shellEnv", "vs/platform/telemetry/common/telemetryService", @@ -30972,7 +34054,8 @@ "vs/base/common/actions", "vs/base/common/platform", "vs/code/electron-sandbox/issue/issueReporterPage", - "vs/code/electron-sandbox/issue/issueReporterService" + "vs/code/electron-sandbox/issue/issueReporterService", + "vs/platform/theme/common/iconRegistry" ], "vs/code/node/sharedProcess/sharedProcessMain": [ "vs/base/common/date", @@ -30996,6 +34079,7 @@ "vs/platform/files/common/io", "vs/platform/files/node/diskFileSystemProvider", "vs/platform/languagePacks/common/languagePacks", + "vs/platform/log/common/log", "vs/platform/remoteTunnel/common/remoteTunnel", "vs/platform/remoteTunnel/node/remoteTunnelService", "vs/platform/request/common/request", @@ -31016,7 +34100,6 @@ "vs/base/browser/ui/actionbar/actionViewItems", "vs/base/browser/ui/findinput/findInput", "vs/base/browser/ui/findinput/findInputToggles", - "vs/base/browser/ui/iconLabel/iconLabelHover", "vs/base/browser/ui/inputbox/inputBox", "vs/base/browser/ui/selectBox/selectBoxCustom", "vs/base/browser/ui/tree/abstractTree", diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index a3f5aeb125e57..ada952513f326 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -46,5 +46,6 @@ export * from './strings'; export * from './telemetry'; export * from './types'; export { default as URI } from './uri'; +export * from './uuid'; export * from './view-column'; export * from './version'; diff --git a/packages/core/src/common/json-schema.ts b/packages/core/src/common/json-schema.ts index 07885c0717a7d..04cb90e30e814 100644 --- a/packages/core/src/common/json-schema.ts +++ b/packages/core/src/common/json-schema.ts @@ -36,6 +36,8 @@ export interface IJSONSchema { $id?: string; $schema?: string; type?: JsonType | JsonType[]; + owner?: string; + group?: string; title?: string; default?: JSONValue; definitions?: IJSONSchemaMap; diff --git a/packages/core/src/common/markdown-rendering/markdown-string.ts b/packages/core/src/common/markdown-rendering/markdown-string.ts index 9f857181685a1..19d93e94132a1 100644 --- a/packages/core/src/common/markdown-rendering/markdown-string.ts +++ b/packages/core/src/common/markdown-rendering/markdown-string.ts @@ -19,9 +19,13 @@ import { UriComponents } from '../uri'; import { escapeIcons } from './icon-utilities'; import { isObject, isString } from '../types'; +export interface MarkdownStringTrustedOptions { + readonly enabledCommands: readonly string[]; +} + export interface MarkdownString { readonly value: string; - readonly isTrusted?: boolean; + readonly isTrusted?: boolean | MarkdownStringTrustedOptions; readonly supportThemeIcons?: boolean; readonly supportHtml?: boolean; readonly baseUri?: UriComponents; @@ -45,9 +49,8 @@ export namespace MarkdownString { // Copied from https://github.com/microsoft/vscode/blob/7d9b1c37f8e5ae3772782ba3b09d827eb3fdd833/src/vs/base/common/htmlContent.ts export class MarkdownStringImpl implements MarkdownString { - public value: string; - public isTrusted?: boolean; + public isTrusted?: boolean | MarkdownStringTrustedOptions; public supportThemeIcons?: boolean; public supportHtml?: boolean; public baseUri?: UriComponents; diff --git a/packages/core/src/common/menu/composite-menu-node.spec.ts b/packages/core/src/common/menu/composite-menu-node.spec.ts new file mode 100644 index 0000000000000..24a002af1a526 --- /dev/null +++ b/packages/core/src/common/menu/composite-menu-node.spec.ts @@ -0,0 +1,67 @@ +// ***************************************************************************** +// 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 { expect } from 'chai'; +import { CompositeMenuNode } from './composite-menu-node'; +import { CompoundMenuNodeRole } from './menu-types'; + +describe('composite-menu-node', () => { + describe('updateOptions', () => { + it('should update undefined node properties', () => { + const node = new CompositeMenuNode('test-id'); + node.updateOptions({ label: 'node-label', icon: 'icon', order: 'a', role: CompoundMenuNodeRole.Flat, when: 'node-condition' }); + expect(node.label).to.equal('node-label'); + expect(node.icon).to.equal('icon'); + expect(node.order).to.equal('a'); + expect(node.role).to.equal(CompoundMenuNodeRole.Flat); + expect(node.when).to.equal('node-condition'); + }); + it('should update existing node properties', () => { + const node = new CompositeMenuNode('test-id', 'test-label', { icon: 'test-icon', order: 'a1', role: CompoundMenuNodeRole.Submenu, when: 'test-condition' }); + node.updateOptions({ label: 'NEW-label', icon: 'NEW-icon', order: 'a2', role: CompoundMenuNodeRole.Flat, when: 'NEW-condition' }); + expect(node.label).to.equal('NEW-label'); + expect(node.icon).to.equal('NEW-icon'); + expect(node.order).to.equal('a2'); + expect(node.role).to.equal(CompoundMenuNodeRole.Flat); + expect(node.when).to.equal('NEW-condition'); + }); + it('should update only the icon without affecting other properties', () => { + const node = new CompositeMenuNode('test-id', 'test-label', { icon: 'test-icon', order: 'a' }); + node.updateOptions({ icon: 'NEW-icon' }); + expect(node.label).to.equal('test-label'); + expect(node.icon).to.equal('NEW-icon'); + expect(node.order).to.equal('a'); + }); + it('should not allow to unset properties', () => { + const node = new CompositeMenuNode('test-id', 'test-label', { icon: 'test-icon', order: 'a' }); + node.updateOptions({ icon: undefined }); + expect(node.label).to.equal('test-label'); + expect(node.icon).to.equal('test-icon'); + expect(node.order).to.equal('a'); + }); + it('should allow to set empty strings in properties', () => { + const node = new CompositeMenuNode('test-id', 'test-label'); + node.updateOptions({ label: '' }); + expect(node.label).to.equal(''); + }); + it('should not cause side effects when updating a property to its existing value', () => { + const node = new CompositeMenuNode('test-id', 'test-label', { icon: 'test-icon', order: 'a' }); + node.updateOptions({ icon: 'test-icon' }); + expect(node.label).to.equal('test-label'); + expect(node.icon).to.equal('test-icon'); + expect(node.order).to.equal('a'); + }); + }); +}); diff --git a/packages/core/src/common/menu/composite-menu-node.ts b/packages/core/src/common/menu/composite-menu-node.ts index aad7208cf9c55..afc1819a3b058 100644 --- a/packages/core/src/common/menu/composite-menu-node.ts +++ b/packages/core/src/common/menu/composite-menu-node.ts @@ -63,11 +63,11 @@ export class CompositeMenuNode implements MutableCompoundMenuNode { updateOptions(options?: SubMenuOptions): void { if (options) { - this.iconClass ??= options.icon ?? options.iconClass; - this.label ??= options.label; - this.order ??= options.order; - this._role ??= options.role; - this._when ??= options.when; + this.iconClass = options.icon ?? options.iconClass ?? this.iconClass; + this.label = options.label ?? this.label; + this.order = options.order ?? this.order; + this._role = options.role ?? this._role; + this._when = options.when ?? this._when; } } diff --git a/packages/core/src/common/menu/menu-model-registry.ts b/packages/core/src/common/menu/menu-model-registry.ts index c29d69ecf3cad..e321440c45170 100644 --- a/packages/core/src/common/menu/menu-model-registry.ts +++ b/packages/core/src/common/menu/menu-model-registry.ts @@ -14,13 +14,14 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject, named } from 'inversify'; -import { Disposable } from '../disposable'; -import { CommandRegistry, Command } from '../command'; +import { inject, injectable, named } from 'inversify'; +import { Command, CommandRegistry } from '../command'; import { ContributionProvider } from '../contribution-provider'; -import { CompositeMenuNode, CompositeMenuNodeWrapper } from './composite-menu-node'; -import { CompoundMenuNode, MenuAction, MenuNode, MenuPath, MutableCompoundMenuNode, SubMenuOptions } from './menu-types'; +import { Disposable } from '../disposable'; +import { Emitter, Event } from '../event'; import { ActionMenuNode } from './action-menu-node'; +import { CompositeMenuNode, CompositeMenuNodeWrapper } from './composite-menu-node'; +import { CompoundMenuNode, MenuAction, MenuNode, MenuNodeMetadata, MenuPath, MutableCompoundMenuNode, SubMenuOptions } from './menu-types'; export const MenuContribution = Symbol('MenuContribution'); @@ -68,6 +69,14 @@ export class MenuModelRegistry { protected readonly root = new CompositeMenuNode(''); protected readonly independentSubmenus = new Map(); + protected readonly onDidChangeEmitter = new Emitter(); + + get onDidChange(): Event { + return this.onDidChangeEmitter.event; + } + + protected isReady = false; + constructor( @inject(ContributionProvider) @named(MenuContribution) protected readonly contributions: ContributionProvider, @@ -78,6 +87,7 @@ export class MenuModelRegistry { for (const contrib of this.contributions.getContributions()) { contrib.registerMenus(this); } + this.isReady = true; } /** @@ -97,7 +107,9 @@ export class MenuModelRegistry { */ registerMenuNode(menuPath: MenuPath | string, menuNode: MenuNode, group?: string): Disposable { const parent = this.getMenuNode(menuPath, group); - return parent.addNode(menuNode); + const disposable = parent.addNode(menuNode); + this.fireChangeEvent(); + return this.changeEventOnDispose(disposable); } getMenuNode(menuPath: MenuPath | string, group?: string): MutableCompoundMenuNode { @@ -137,13 +149,15 @@ export class MenuModelRegistry { const groupPath = index === 0 ? [] : menuPath.slice(0, index); const parent = this.findGroup(groupPath, options); let groupNode = this.findSubMenu(parent, menuId, options); + let disposable = Disposable.NULL; if (!groupNode) { groupNode = new CompositeMenuNode(menuId, label, options, parent); - return parent.addNode(groupNode); + disposable = this.changeEventOnDispose(parent.addNode(groupNode)); } else { groupNode.updateOptions({ ...options, label }); - return Disposable.NULL; } + this.fireChangeEvent(); + return disposable; } registerIndependentSubmenu(id: string, label: string, options?: SubMenuOptions): Disposable { @@ -151,14 +165,33 @@ export class MenuModelRegistry { console.debug(`Independent submenu with path ${id} registered, but given ID already exists.`); } this.independentSubmenus.set(id, new CompositeMenuNode(id, label, options)); - return { dispose: () => this.independentSubmenus.delete(id) }; + return this.changeEventOnDispose(Disposable.create(() => this.independentSubmenus.delete(id))); } linkSubmenu(parentPath: MenuPath | string, childId: string | MenuPath, options?: SubMenuOptions, group?: string): Disposable { const child = this.getMenuNode(childId); const parent = this.getMenuNode(parentPath, group); + + const isRecursive = (node: MenuNodeMetadata, childNode: MenuNodeMetadata): boolean => { + if (node.id === childNode.id) { + return true; + } + if (node.parent) { + return isRecursive(node.parent, childNode); + } + return false; + }; + + // check for menu contribution recursion + if (isRecursive(parent, child)) { + console.warn(`Recursive menu contribution detected: ${child.id} is already in hierarchy of ${parent.id}.`); + return Disposable.NULL; + } + const wrapper = new CompositeMenuNodeWrapper(child, parent, options); - return parent.addNode(wrapper); + const disposable = parent.addNode(wrapper); + this.fireChangeEvent(); + return this.changeEventOnDispose(disposable); } /** @@ -190,6 +223,7 @@ export class MenuModelRegistry { if (menuPath) { const parent = this.findGroup(menuPath); parent.removeNode(id); + this.fireChangeEvent(); return; } @@ -211,6 +245,7 @@ export class MenuModelRegistry { }); }; recurse(this.root); + this.fireChangeEvent(); } /** @@ -304,6 +339,19 @@ export class MenuModelRegistry { return true; } + protected changeEventOnDispose(disposable: Disposable): Disposable { + return Disposable.create(() => { + disposable.dispose(); + this.fireChangeEvent(); + }); + } + + protected fireChangeEvent(): void { + if (this.isReady) { + this.onDidChangeEmitter.fire(); + } + } + /** * Returns the {@link MenuPath path} at which a given menu node can be accessed from this registry, if it can be determined. * Returns `undefined` if the `parent` of any node in the chain is unknown. diff --git a/packages/core/src/common/menu/menu-types.ts b/packages/core/src/common/menu/menu-types.ts index 23081a88e3ad1..b3a443816a010 100644 --- a/packages/core/src/common/menu/menu-types.ts +++ b/packages/core/src/common/menu/menu-types.ts @@ -68,6 +68,7 @@ export interface MenuNodeBase extends MenuNodeMetadata, MenuNodeRenderingData { * A menu entry representing an action, e.g. "New File". */ export interface MenuAction extends MenuNodeRenderingData, Pick { + /** * The command to execute. */ diff --git a/packages/core/src/common/menu/menu.spec.ts b/packages/core/src/common/menu/menu.spec.ts index 78b769c4d0da8..650ae274574d0 100644 --- a/packages/core/src/common/menu/menu.spec.ts +++ b/packages/core/src/common/menu/menu.spec.ts @@ -14,10 +14,10 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { CommandContribution, CommandRegistry } from '../command'; -import { MenuContribution, MenuModelRegistry } from './menu-model-registry'; import * as chai from 'chai'; +import { CommandContribution, CommandRegistry } from '../command'; import { CompositeMenuNode } from './composite-menu-node'; +import { MenuContribution, MenuModelRegistry } from './menu-model-registry'; const expect = chai.expect; @@ -61,6 +61,25 @@ describe('menu-model-registry', () => { expect(openGroup.children.length).equals(2); expect(openGroup.label).undefined; }); + + it('Should not allow to register cyclic menus.', () => { + const fileMenu = ['main', 'File']; + const fileOpenMenu = [...fileMenu, '0_open']; + const fileCloseMenu = [...fileMenu, '1_close']; + const service = createMenuRegistry({ + registerMenus(menuRegistry: MenuModelRegistry): void { + menuRegistry.registerSubmenu(fileMenu, 'File'); + // open menu should not be added to open menu + menuRegistry.linkSubmenu(fileOpenMenu, fileOpenMenu); + // close menu should be added + menuRegistry.linkSubmenu(fileOpenMenu, fileCloseMenu); + } + }, { + registerCommands(reg: CommandRegistry): void { } + }); + const all = service.getMenu() as CompositeMenuNode; + expect(menuStructureToString(all.children[0] as CompositeMenuNode)).equals('File(0_open(1_close),1_close())'); + }); }); }); @@ -71,3 +90,12 @@ function createMenuRegistry(menuContrib: MenuContribution, commandContrib: Comma menuReg.onStart(); return menuReg; } + +function menuStructureToString(node: CompositeMenuNode): string { + return node.children.map(c => { + if (c instanceof CompositeMenuNode) { + return `${c.id}(${menuStructureToString(c)})`; + } + return c.id; + }).join(','); +} diff --git a/packages/core/src/common/message-rpc/channel.ts b/packages/core/src/common/message-rpc/channel.ts index 05a9ba2426dfc..1f2b5047801d8 100644 --- a/packages/core/src/common/message-rpc/channel.ts +++ b/packages/core/src/common/message-rpc/channel.ts @@ -220,6 +220,8 @@ export class ChannelMultiplexer implements Disposable { this.openChannels.set(id, channel); resolve(channel); this.onOpenChannelEmitter.fire({ id, channel }); + } else { + console.error(`not expecting ack-open on for ${id}`); } } @@ -234,6 +236,8 @@ export class ChannelMultiplexer implements Disposable { } this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.AckOpen).writeString(id).commit(); this.onOpenChannelEmitter.fire({ id, channel }); + } else { + console.error(`channel already open: ${id}`); } } @@ -275,7 +279,7 @@ export class ChannelMultiplexer implements Disposable { } open(id: string): Promise { - if (this.openChannels.has(id)) { + if (this.openChannels.has(id) || this.pendingOpen.has(id)) { throw new Error(`Another channel with the id '${id}' is already open.`); } const result = new Promise((resolve, reject) => { diff --git a/packages/core/src/common/message-rpc/message-buffer.ts b/packages/core/src/common/message-rpc/message-buffer.ts index d0b2fad0e351a..a27c41f3c77ca 100644 --- a/packages/core/src/common/message-rpc/message-buffer.ts +++ b/packages/core/src/common/message-rpc/message-buffer.ts @@ -25,6 +25,7 @@ export interface WriteBuffer { writeBytes(value: Uint8Array): this writeNumber(value: number): this writeLength(value: number): this + writeRaw(bytes: Uint8Array): this; /** * Makes any writes to the buffer permanent, for example by sending the writes over a channel. * You must obtain a new write buffer after committing @@ -71,6 +72,11 @@ export class ForwardingWriteBuffer implements WriteBuffer { return this; } + writeRaw(bytes: Uint8Array): this { + this.underlying.writeRaw(bytes); + return this; + } + commit(): void { this.underlying.commit(); } diff --git a/packages/core/src/common/message-rpc/msg-pack-extension-manager.ts b/packages/core/src/common/message-rpc/msg-pack-extension-manager.ts index 5844c1f684bfb..c6be687ae6ec5 100644 --- a/packages/core/src/common/message-rpc/msg-pack-extension-manager.ts +++ b/packages/core/src/common/message-rpc/msg-pack-extension-manager.ts @@ -21,7 +21,7 @@ import { addExtension } from 'msgpackr'; * required for the default RPC communication. MsgPackR extensions * are installed globally on both ends of the communication channel. * (frontend-backend, pluginExt-pluginMain). - * Is implemented as singleton as it is also used in plugin child processes which have no access to inversify. + * Is implemented as singleton as it is also used in plugin child processes which have no access to inversify. */ export class MsgPackExtensionManager { private static readonly INSTANCE = new MsgPackExtensionManager(); diff --git a/packages/core/src/common/message-rpc/rpc-protocol.ts b/packages/core/src/common/message-rpc/rpc-protocol.ts index 9ebd529c4a3ed..f3f51decb3270 100644 --- a/packages/core/src/common/message-rpc/rpc-protocol.ts +++ b/packages/core/src/common/message-rpc/rpc-protocol.ts @@ -16,7 +16,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { CancellationToken, CancellationTokenSource } from '../cancellation'; -import { Disposable, DisposableCollection } from '../disposable'; +import { DisposableWrapper, Disposable, DisposableCollection } from '../disposable'; import { Emitter, Event } from '../event'; import { Deferred } from '../promise-util'; import { Channel } from './channel'; @@ -57,6 +57,7 @@ export class RpcProtocol { static readonly CANCELLATION_TOKEN_KEY = 'add.cancellation.token'; protected readonly pendingRequests: Map> = new Map(); + protected readonly pendingRequestCancellationEventListeners: Map = new Map(); protected nextMessageId: number = 0; @@ -80,6 +81,8 @@ export class RpcProtocol { channel.onClose(event => { this.pendingRequests.forEach(pending => pending.reject(new Error(event.reason))); this.pendingRequests.clear(); + this.pendingRequestCancellationEventListeners.forEach(disposable => disposable.dispose()); + this.pendingRequestCancellationEventListeners.clear(); this.toDispose.dispose(); }); this.toDispose.push(channel.onMessage(readBuffer => this.handleMessage(this.decoder.parse(readBuffer())))); @@ -131,6 +134,7 @@ export class RpcProtocol { } else { throw new Error(`No reply handler for reply with id: ${id}`); } + this.disposeCancellationEventListener(id); } protected handleReplyErr(id: number, error: any): void { @@ -141,6 +145,15 @@ export class RpcProtocol { } else { throw new Error(`No reply handler for error reply with id: ${id}`); } + this.disposeCancellationEventListener(id); + } + + protected disposeCancellationEventListener(id: number): void { + const toDispose = this.pendingRequestCancellationEventListeners.get(id); + if (toDispose) { + this.pendingRequestCancellationEventListeners.delete(id); + toDispose.dispose(); + } } sendRequest(method: string, args: any[]): Promise { @@ -157,6 +170,10 @@ export class RpcProtocol { this.pendingRequests.set(id, reply); + // register disposable before output.commit() even when not available yet + const disposableWrapper = new DisposableWrapper(); + this.pendingRequestCancellationEventListeners.set(id, disposableWrapper); + const output = this.channel.getWriteBuffer(); this.encoder.request(output, id, method, args); output.commit(); @@ -164,7 +181,10 @@ export class RpcProtocol { if (cancellationToken?.isCancellationRequested) { this.sendCancel(id); } else { - cancellationToken?.onCancellationRequested(() => this.sendCancel(id)); + const disposable = cancellationToken?.onCancellationRequested(() => this.sendCancel(id)); + if (disposable) { + disposableWrapper.set(disposable); + } } return reply.promise; diff --git a/packages/core/src/common/message-rpc/uint8-array-message-buffer.ts b/packages/core/src/common/message-rpc/uint8-array-message-buffer.ts index 5b4294b3d57aa..af07a35a48461 100644 --- a/packages/core/src/common/message-rpc/uint8-array-message-buffer.ts +++ b/packages/core/src/common/message-rpc/uint8-array-message-buffer.ts @@ -76,6 +76,13 @@ export class Uint8ArrayWriteBuffer implements WriteBuffer, Disposable { return this; } + writeRaw(bytes: Uint8Array): this { + this.ensureCapacity(bytes.byteLength); + this.buffer.set(bytes, this.offset); + this.offset += bytes.byteLength; + return this; + } + writeUint16(value: number): this { this.ensureCapacity(2); this.msg.setUint16(this.offset, value); diff --git a/packages/core/src/common/messaging/abstract-connection-provider.ts b/packages/core/src/common/messaging/abstract-connection-provider.ts deleted file mode 100644 index 2e2c8c8956232..0000000000000 --- a/packages/core/src/common/messaging/abstract-connection-provider.ts +++ /dev/null @@ -1,115 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2020 Ericsson 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 { injectable, interfaces } from 'inversify'; -import { Emitter, Event } from '../event'; -import { ConnectionHandler } from './handler'; -import { RpcProxy, RpcProxyFactory } from './proxy-factory'; -import { Channel, ChannelMultiplexer } from '../message-rpc/channel'; - -/** - * Factor common logic according to `ElectronIpcConnectionProvider` and - * `WebSocketConnectionProvider`. This class handles channels in a somewhat - * generic way. - */ -@injectable() -export abstract class AbstractConnectionProvider { - - /** - * Create a proxy object to remote interface of T type - * over an electron ipc connection for the given path and proxy factory. - */ - static createProxy(container: interfaces.Container, path: string, factory: RpcProxyFactory): RpcProxy; - /** - * Create a proxy object to remote interface of T type - * over an electron ipc connection for the given path. - * - * An optional target can be provided to handle - * notifications and requests from a remote side. - */ - static createProxy(container: interfaces.Container, path: string, target?: object): RpcProxy { - throw new Error('abstract'); - } - - protected readonly onIncomingMessageActivityEmitter: Emitter = new Emitter(); - get onIncomingMessageActivity(): Event { - return this.onIncomingMessageActivityEmitter.event; - } - - /** - * Create a proxy object to remote interface of T type - * over a web socket connection for the given path and proxy factory. - */ - createProxy(path: string, factory: RpcProxyFactory): RpcProxy; - /** - * Create a proxy object to remote interface of T type - * over a web socket connection for the given path. - * - * An optional target can be provided to handle - * notifications and requests from a remote side. - */ - createProxy(path: string, target?: object): RpcProxy; - createProxy(path: string, arg?: object): RpcProxy { - const factory = arg instanceof RpcProxyFactory ? arg : new RpcProxyFactory(arg); - this.listen({ - path, - onConnection: c => factory.listen(c) - }); - return factory.createProxy(); - } - - protected channelMultiplexer?: ChannelMultiplexer; - - // A set of channel opening functions that are executed if the backend reconnects to restore the - // the channels that were open before the disconnect occurred. - protected reconnectChannelOpeners: Array<() => Promise> = []; - - protected initializeMultiplexer(): void { - const mainChannel = this.createMainChannel(); - mainChannel.onMessage(() => this.onIncomingMessageActivityEmitter.fire()); - this.channelMultiplexer = new ChannelMultiplexer(mainChannel); - } - - /** - * Install a connection handler for the given path. - */ - listen(handler: ConnectionHandler, options?: AbstractOptions): void { - this.openChannel(handler.path, channel => { - handler.onConnection(channel); - }, options); - } - - async openChannel(path: string, handler: (channel: Channel) => void, options?: AbstractOptions): Promise { - if (!this.channelMultiplexer) { - throw new Error('The channel multiplexer has not been initialized yet!'); - } - const newChannel = await this.channelMultiplexer.open(path); - newChannel.onClose(() => { - const { reconnecting } = { reconnecting: true, ...options }; - if (reconnecting) { - this.reconnectChannelOpeners.push(() => this.openChannel(path, handler, options)); - } - }); - - handler(newChannel); - } - - /** - * Create the main connection that is used for multiplexing all service channels. - */ - protected abstract createMainChannel(): Channel; - -} diff --git a/packages/core/src/common/messaging/connection-management.ts b/packages/core/src/common/messaging/connection-management.ts new file mode 100644 index 0000000000000..6c019401d00ff --- /dev/null +++ b/packages/core/src/common/messaging/connection-management.ts @@ -0,0 +1,43 @@ + +// ***************************************************************************** +// Copyright (C) 2017 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 +// ***************************************************************************** + +export const ConnectionCloseService = Symbol('ConnectionCloseService'); +export const connectionCloseServicePath = '/services/ChannelCloseService'; + +/** + * These messages are used to negotiate service reconnection between a front ends and back end. + * Whenever a front end first connects to a back end, it sends the ${@link ConnectionManagementMessages#INITIAL_CONNECT} message + * together with its front end id. + * The back end then starts a new front end connection context for that front end. If the back end already had another connection context + * for the given front end id, it gets discarded. + * If the front end reconnects after a websocket disconnect, it sends the ${@link ConnectionManagementMessages#RECONNECT} message + * together with its front end id.. + * If the back end still has a connection context for the front end id, the context is reconnected and the back end replies with the value true. + * If there is no context anymore, the back end replies with value false. The front end can then either do an initial connect or reload + * the whole UI. + */ +export namespace ConnectionManagementMessages { + export const INITIAL_CONNECT = 'initialConnection'; + export const RECONNECT = 'reconnect'; +} + +/** + * A service to mark a front end as unused. As soon as it disconnects from the back end, the connection context will be discarded. + */ +export interface ConnectionCloseService { + markForClose(frontEndId: string): Promise; +} diff --git a/packages/core/src/common/messaging/handler.ts b/packages/core/src/common/messaging/handler.ts index 204125be8a203..0bdfac3e34e47 100644 --- a/packages/core/src/common/messaging/handler.ts +++ b/packages/core/src/common/messaging/handler.ts @@ -16,6 +16,8 @@ import { Channel } from '../message-rpc/channel'; +export const servicesPath = '/services'; + export const ConnectionHandler = Symbol('ConnectionHandler'); export interface ConnectionHandler { diff --git a/packages/core/src/common/messaging/proxy-factory.ts b/packages/core/src/common/messaging/proxy-factory.ts index ab654ca871a46..f24f6bbb1bdd7 100644 --- a/packages/core/src/common/messaging/proxy-factory.ts +++ b/packages/core/src/common/messaging/proxy-factory.ts @@ -168,14 +168,7 @@ export class RpcProxyFactory implements ProxyHandler { throw new Error(`no target was set to handle ${method}`); } } catch (error) { - const e = this.serializeError(error); - if (e instanceof ResponseError) { - throw e; - } - const reason = e.message || ''; - const stack = e.stack || ''; - console.error(`Request ${method} failed with error: ${reason}`, stack); - throw e; + throw this.serializeError(error); } } diff --git a/packages/core/src/common/messaging/socket-write-buffer.ts b/packages/core/src/common/messaging/socket-write-buffer.ts new file mode 100644 index 0000000000000..36d8dc17f750b --- /dev/null +++ b/packages/core/src/common/messaging/socket-write-buffer.ts @@ -0,0 +1,52 @@ +// ***************************************************************************** +// Copyright (C) 2018 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 { WebSocket } from './web-socket-channel'; + +export class SocketWriteBuffer { + private static DISCONNECTED_BUFFER_SIZE = 100 * 1024; + + private disconnectedBuffer: Uint8Array | undefined; + private bufferWritePosition = 0; + + buffer(data: Uint8Array): void { + this.ensureWriteBuffer(data.byteLength); + this.disconnectedBuffer?.set(data, this.bufferWritePosition); + this.bufferWritePosition += data.byteLength; + } + + protected ensureWriteBuffer(byteLength: number): void { + if (!this.disconnectedBuffer) { + this.disconnectedBuffer = new Uint8Array(SocketWriteBuffer.DISCONNECTED_BUFFER_SIZE); + this.bufferWritePosition = 0; + } + + if (this.bufferWritePosition + byteLength > this.disconnectedBuffer.byteLength) { + throw new Error(`Max disconnected buffer size exceeded by adding ${byteLength} bytes`); + } + } + + flush(socket: WebSocket): void { + if (this.disconnectedBuffer) { + socket.send(this.disconnectedBuffer.slice(0, this.bufferWritePosition)); + this.disconnectedBuffer = undefined; + } + } + + drain(): void { + this.disconnectedBuffer = undefined; + } +} diff --git a/packages/core/src/common/messaging/web-socket-channel.ts b/packages/core/src/common/messaging/web-socket-channel.ts index 4f98d5269fb32..9b4c61fee9d7e 100644 --- a/packages/core/src/common/messaging/web-socket-channel.ts +++ b/packages/core/src/common/messaging/web-socket-channel.ts @@ -19,7 +19,11 @@ import { WriteBuffer } from '../message-rpc'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../message-rpc/uint8-array-message-buffer'; import { AbstractChannel } from '../message-rpc/channel'; -import { Disposable } from '../disposable'; +import { Socket as ClientSocket } from 'socket.io-client'; +import { Socket as ServerSocket } from 'socket.io'; +import { Emitter } from 'vscode-languageserver-protocol'; + +export type WebSocket = ClientSocket | ServerSocket; /** * A channel that manages the main websocket connection between frontend and backend. All service channels @@ -29,65 +33,44 @@ import { Disposable } from '../disposable'; export class WebSocketChannel extends AbstractChannel { static wsPath = '/services'; - constructor(protected readonly socket: IWebSocket) { + private onDidConnectEmitter = new Emitter(); + onDidConnect = this.onDidConnectEmitter.event; + + constructor(protected readonly socket: WebSocket) { super(); - this.toDispose.push(Disposable.create(() => socket.close())); - socket.onClose((reason, code) => this.onCloseEmitter.fire({ reason, code })); - socket.onClose(() => this.close()); - socket.onError(error => this.onErrorEmitter.fire(error)); - socket.onMessage(data => this.onMessageEmitter.fire(() => { + socket.on('connect', () => { + this.onDidConnectEmitter.fire(); + }); + + socket.on('disconnect', reason => { + this.onCloseEmitter.fire({ + reason: reason + }); + }); + + socket.on('error', reason => this.onErrorEmitter.fire(reason)); + socket.on('message', data => { // In the browser context socketIO receives binary messages as ArrayBuffers. // So we have to convert them to a Uint8Array before delegating the message to the read buffer. const buffer = data instanceof ArrayBuffer ? new Uint8Array(data) : data; - return new Uint8ArrayReadBuffer(buffer); - })); + this.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(buffer)); + }); } getWriteBuffer(): WriteBuffer { const result = new Uint8ArrayWriteBuffer(); result.onCommit(buffer => { - if (this.socket.isConnected()) { + if (this.socket.connected) { this.socket.send(buffer); } }); return result; } -} -/** - * An abstraction that enables reuse of the `{@link WebSocketChannel} class in the frontend and backend - * independent of the actual underlying socket implementation. - */ -export interface IWebSocket { - /** - * Sends the given message over the web socket in binary format. - * @param message The binary message. - */ - send(message: Uint8Array): void; - /** - * Closes the websocket from the local side. - */ - close(): void; - /** - * The connection state of the web socket. - */ - isConnected(): boolean; - /** - * Listener callback to handle incoming messages. - * @param cb The callback. - */ - onMessage(cb: (message: Uint8Array) => void): void; - /** - * Listener callback to handle socket errors. - * @param cb The callback. - */ - onError(cb: (reason: any) => void): void; - /** - * Listener callback to handle close events (Remote side). - * @param cb The callback. - */ - onClose(cb: (reason: string, code?: number) => void): void; + override close(): void { + this.socket.disconnect(); + super.close(); + } } - diff --git a/packages/core/src/common/nls.ts b/packages/core/src/common/nls.ts index f829f57623d27..acdc62474e419 100644 --- a/packages/core/src/common/nls.ts +++ b/packages/core/src/common/nls.ts @@ -83,7 +83,9 @@ class LocalizationKeyProvider { private preferredKeys = new Set([ // We only want the `File` translation used in the menu - 'vscode/fileActions.contribution/filesCategory' + 'vscode/fileActions.contribution/filesCategory', + // Needed for `Close Editor` translation + 'vscode/editor.contribution/closeEditor' ]); private data = this.buildData(); diff --git a/packages/core/src/common/preferences/preference-schema.ts b/packages/core/src/common/preferences/preference-schema.ts index 3d1e15ca0411a..436a9f82a1d38 100644 --- a/packages/core/src/common/preferences/preference-schema.ts +++ b/packages/core/src/common/preferences/preference-schema.ts @@ -25,6 +25,11 @@ export interface PreferenceSchema { [name: string]: any, scope?: 'application' | 'window' | 'resource' | PreferenceScope, overridable?: boolean; + /** + * The title of the preference schema. + * It is used in the preference UI to associate a localized group of preferences. + */ + title?: string; properties: PreferenceSchemaProperties } export namespace PreferenceSchema { @@ -75,6 +80,7 @@ export interface PreferenceSchemaProperty extends PreferenceItem { description?: string; markdownDescription?: string; scope?: 'application' | 'machine' | 'window' | 'resource' | 'language-overridable' | 'machine-overridable' | PreferenceScope; + tags?: string[]; } export interface PreferenceDataProperty extends PreferenceItem { diff --git a/packages/core/src/common/promise-util.spec.ts b/packages/core/src/common/promise-util.spec.ts index 051cbd4fa78c7..f3b3236a9962e 100644 --- a/packages/core/src/common/promise-util.spec.ts +++ b/packages/core/src/common/promise-util.spec.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import * as assert from 'assert/strict'; -import { firstTrue, waitForEvent } from './promise-util'; +import { Deferred, firstTrue, waitForEvent } from './promise-util'; import { Emitter } from './event'; import { CancellationError } from './cancellation'; @@ -35,7 +35,30 @@ describe('promise-util', () => { }); }); + type ExecutionHandler = (resolve: (value: T) => void, reject: (error: unknown) => void) => void; + describe('firstTrue', () => { + function createSequentialPromises(...executionHandlers: ExecutionHandler[]): Promise[] { + const deferreds: Deferred[] = []; + let i = 0; + for (let k = 0; k < executionHandlers.length; k++) { + deferreds.push(new Deferred()); + } + + const resolveNext = () => { + if (i < executionHandlers.length) { + executionHandlers[i](value => deferreds[i].resolve(value), error => deferreds[i].reject(error)); + i++; + } + if (i < executionHandlers.length) { + setTimeout(resolveNext, 1); + } + }; + + setTimeout(resolveNext, 1); + return deferreds.map(deferred => deferred.promise); + } + it('should resolve to false when the promises arg is empty', async () => { const actual = await firstTrue(); assert.strictEqual(actual, false); @@ -43,29 +66,36 @@ describe('promise-util', () => { it('should resolve to true when the first promise resolves to true', async () => { const signals: string[] = []; - const createPromise = (signal: string, timeout: number, result: boolean) => - new Promise(resolve => setTimeout(() => { + + function createHandler(signal: string, result?: boolean): ExecutionHandler { + return (resolve: (value: boolean) => void, reject: (error: unknown) => void) => { signals.push(signal); - resolve(result); - }, timeout)); - const actual = await firstTrue( - createPromise('a', 10, false), - createPromise('b', 20, false), - createPromise('c', 30, true), - createPromise('d', 40, false), - createPromise('e', 50, true) - ); + if (typeof result !== 'undefined') { + resolve(result); + } else { + reject(undefined); + } + }; + } + + const actual = await firstTrue(...createSequentialPromises( + createHandler('a', false), + createHandler('b', false), + createHandler('c', true), + createHandler('d', false), + createHandler('e', true) + )); assert.strictEqual(actual, true); assert.deepStrictEqual(signals, ['a', 'b', 'c']); }); it('should reject when one of the promises rejects', async () => { - await assert.rejects(firstTrue( - new Promise(resolve => setTimeout(() => resolve(false), 10)), - new Promise(resolve => setTimeout(() => resolve(false), 20)), - new Promise((_, reject) => setTimeout(() => reject(new Error('my test error')), 30)), - new Promise(resolve => setTimeout(() => resolve(true), 40)), - ), /Error: my test error/); + await assert.rejects(firstTrue(...createSequentialPromises( + (resolve, _) => resolve(false), + resolve => resolve(false), + (_, reject) => reject(new Error('my test error')), + resolve => resolve(true), + )), /Error: my test error/); }); }); diff --git a/packages/core/src/common/quick-pick-service.ts b/packages/core/src/common/quick-pick-service.ts index 96a1aba08bd57..dd3b07e1f8e1f 100644 --- a/packages/core/src/common/quick-pick-service.ts +++ b/packages/core/src/common/quick-pick-service.ts @@ -14,12 +14,10 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import URI from './uri'; import * as fuzzy from 'fuzzy'; import { Event } from './event'; import { KeySequence } from './keys'; import { CancellationToken } from './cancellation'; -import { URI as Uri } from 'vscode-uri'; export const quickPickServicePath = '/services/quickPick'; export const QuickPickService = Symbol('QuickPickService'); @@ -54,11 +52,10 @@ export interface QuickPickItem { description?: string; detail?: string; keySequence?: KeySequence; - iconPath?: URI | Uri | { light?: URI | Uri; dark: URI | Uri } | { id: string }; iconClasses?: string[]; alwaysShow?: boolean; highlights?: QuickPickItemHighlights; - buttons?: readonly QuickInputButton[]; + buttons?: QuickInputButton[]; execute?: () => void; } @@ -95,7 +92,6 @@ export interface QuickPickValue extends QuickPickItem { } export interface QuickInputButton { - iconPath?: URI | Uri | { light?: URI | Uri; dark: URI | Uri } | { id: string }; iconClass?: string; tooltip?: string; /** @@ -104,34 +100,8 @@ export interface QuickInputButton { alwaysVisible?: boolean; } -export interface NormalizedQuickInputButton extends QuickInputButton { - iconPath?: { light?: Uri, dark: Uri }; -} - -export namespace QuickInputButton { - export function normalize(button: undefined): undefined; - export function normalize(button: QuickInputButton): NormalizedQuickInputButton; - export function normalize(button?: QuickInputButton): NormalizedQuickInputButton | undefined { - if (!button) { - return button; - } - let iconPath: NormalizedQuickInputButton['iconPath'] = undefined; - if (button.iconPath instanceof URI) { - iconPath = { dark: button.iconPath['codeUri'] }; - } else if (button.iconPath && 'dark' in button.iconPath) { - const dark = Uri.isUri(button.iconPath.dark) ? button.iconPath.dark : button.iconPath.dark['codeUri']; - const light = Uri.isUri(button.iconPath.light) ? button.iconPath.light : button.iconPath.light?.['codeUri']; - iconPath = { dark, light }; - } - return { - ...button, - iconPath, - }; - } -} - export interface QuickInputButtonHandle extends QuickInputButton { - index: number; // index of where they are in buttons array if QuickInputButton or -1 if QuickInputButtons.Back + handle: number; // index of where the button is in buttons array if QuickInputButton or -1 if QuickInputButtons.Back } export enum QuickInputHideReason { @@ -188,10 +158,11 @@ export interface QuickPick extends QuickInpu matchOnDescription: boolean; matchOnDetail: boolean; keepScrollPosition: boolean; + buttons: ReadonlyArray; readonly onDidAccept: Event<{ inBackground: boolean } | undefined>; readonly onDidChangeValue: Event; readonly onDidTriggerButton: Event; - readonly onDidTriggerItemButton: Event>; + readonly onDidTriggerItemButton: Event>; readonly onDidChangeActive: Event; readonly onDidChangeSelection: Event; } @@ -281,8 +252,13 @@ export interface QuickInputService { open(filter: string): void; createInputBox(): InputBox; input(options?: InputOptions, token?: CancellationToken): Promise; - pick>(picks: Promise[]> | QuickPickInput[], options?: O, token?: CancellationToken): - Promise<(O extends { canPickMany: true } ? T[] : T) | undefined>; + pick(picks: Promise[]> | QuickPickInput[], + options?: PickOptions & { canPickMany: true }, token?: CancellationToken): Promise; + pick(picks: Promise[]> | QuickPickInput[], + options?: PickOptions & { canPickMany: false }, token?: CancellationToken): Promise; + pick(picks: Promise[]> | QuickPickInput[], + options?: Omit, 'canPickMany'>, token?: CancellationToken): Promise; + showQuickPick(items: Array, options?: QuickPickOptions): Promise; hide(): void; /** diff --git a/packages/core/src/common/reference.ts b/packages/core/src/common/reference.ts index f8a77ec4f0b3a..bf6fe4a888e24 100644 --- a/packages/core/src/common/reference.ts +++ b/packages/core/src/common/reference.ts @@ -22,6 +22,50 @@ export interface Reference extends Disposable { readonly object: T } +/** + * Abstract class for a map of reference-counted disposable objects, with the + * following features: + * + * - values are not inserted explicitly; instead, acquire() is used to + * create the value for a given key, or return the previously created + * value for it. How the value is created for a given key is + * implementation specific. + * + * - any subsquent acquire() with the same key will bump the reference + * count on that value. acquire() returns not the value directly but + * a reference object that holds the value. Calling dispose() on the + * reference decreases the value's effective reference count. + * + * - a contained value will have its dispose() function called when its + * reference count reaches zero. The key/value pair will be purged + * from the collection. + * + * - calling dispose() on the value directly, instead of calling it on + * the reference returned by acquire(), will automatically dispose + * all outstanding references to that value and the key/value pair + * will be purged from the collection. + * + * - supports synchronous and asynchronous implementations. acquire() will + * return a Promise if the value cannot be created immediately + * + * - functions has|keys|values|get are always synchronous and the result + * excludes asynchronous additions in flight. + * + * - functions values|get return the value directly and not a reference + * to the value. Use these functions to obtain a value without bumping + * its reference count. + * + * - clients can register to be notified when values are added and removed; + * notification for asynchronous additions happen when the creation + * completes, not when it's requested. + * + * - keys can be any value/object that can be successfully stringified using + * JSON.stringify(), sans arguments + * + * - calling dispose() on the collection will dispose all outstanding + * references to all contained values, which results in the disposal of + * the values themselves. + */ export abstract class AbstractReferenceCollection implements Disposable { protected readonly _keys = new Map(); @@ -108,6 +152,12 @@ export abstract class AbstractReferenceCollection imple } +/** + * Asynchronous implementation of AbstractReferenceCollection that requires + * the client to provide a value factory, used to service the acquire() + * function. That factory may return a Promise if the value cannot be + * created immediately. + */ export class ReferenceCollection extends AbstractReferenceCollection { constructor(protected readonly factory: (key: K) => MaybePromise) { @@ -148,6 +198,11 @@ export class ReferenceCollection extends AbstractRefere } +/** + * Synchronous implementation of AbstractReferenceCollection that requires + * the client to provide a value factory, used to service the acquire() + * function. + */ export class SyncReferenceCollection extends AbstractReferenceCollection { constructor(protected readonly factory: (key: K) => V) { diff --git a/packages/core/src/common/resource.ts b/packages/core/src/common/resource.ts index 4c62063e94478..abe188733a925 100644 --- a/packages/core/src/common/resource.ts +++ b/packages/core/src/common/resource.ts @@ -25,6 +25,7 @@ import { CancellationToken } from './cancellation'; import { ApplicationError } from './application-error'; import { ReadableStream, Readable } from './stream'; import { SyncReferenceCollection, Reference } from './reference'; +import { MarkdownString } from './markdown-rendering'; export interface ResourceVersion { } @@ -55,7 +56,10 @@ export interface Resource extends Disposable { * Undefined if a resource did not read content yet. */ readonly encoding?: string | undefined; - readonly isReadonly?: boolean; + + readonly onDidChangeReadOnly?: Event; + + readonly readOnly?: boolean | MarkdownString; /** * Reads latest content of this resource. * diff --git a/packages/core/src/common/selection-command-handler.ts b/packages/core/src/common/selection-command-handler.ts index 3e32a33928cca..9764200c0aba8 100644 --- a/packages/core/src/common/selection-command-handler.ts +++ b/packages/core/src/common/selection-command-handler.ts @@ -18,7 +18,7 @@ import { CommandHandler } from './command'; import { SelectionService } from '../common/selection-service'; -export class SelectionCommandHandler implements CommandHandler { +export class SelectionCommandHandler implements CommandHandler { constructor( protected readonly selectionService: SelectionService, diff --git a/packages/core/src/common/theme.ts b/packages/core/src/common/theme.ts index e6b5e737c8e16..8350011737611 100644 --- a/packages/core/src/common/theme.ts +++ b/packages/core/src/common/theme.ts @@ -14,6 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { URI } from 'vscode-uri'; + export type ThemeType = 'light' | 'dark' | 'hc' | 'hcLight'; export interface Theme { @@ -34,3 +36,33 @@ export interface ThemeChangeEvent { readonly newTheme: Theme; readonly oldTheme?: Theme; } + +export interface ThemeColor { + id: string; +} + +export interface ThemeIcon { + readonly id: string; + readonly color?: ThemeColor; +} + +export interface IconDefinition { + font?: IconFontContribution; // undefined for the default font (codicon) + fontCharacter: string; +} + +export interface IconFontContribution { + readonly id: string; + readonly definition: IconFontDefinition; +} + +export interface IconFontDefinition { + readonly weight?: string; + readonly style?: string; + readonly src: IconFontSource[]; +} + +export interface IconFontSource { + readonly location: URI; + readonly format: string; +} diff --git a/packages/core/src/common/uri.ts b/packages/core/src/common/uri.ts index fe24bc2e54339..5816c57cae48f 100644 --- a/packages/core/src/common/uri.ts +++ b/packages/core/src/common/uri.ts @@ -19,8 +19,10 @@ import { Path } from './path'; export class URI { - public static fromComponents(components: UriComponents): URI { - return new URI(Uri.revive(components)); + public static fromComponents(components: UriComponents): URI; + public static fromComponents(components: undefined): undefined; + public static fromComponents(components: UriComponents | undefined): URI | undefined { + return components ? new URI(Uri.revive(components)) : undefined; } public static fromFilePath(path: string): URI { diff --git a/packages/core/src/common/uuid.ts b/packages/core/src/common/uuid.ts index 1325fa7249d83..41561cebc593a 100644 --- a/packages/core/src/common/uuid.ts +++ b/packages/core/src/common/uuid.ts @@ -21,79 +21,25 @@ // based on https://github.com/microsoft/vscode/blob/1.72.2/src/vs/base/common/uuid.ts +import { v4, v5 } from 'uuid'; + const _UUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export function isUUID(value: string): boolean { return _UUIDPattern.test(value); } -declare const crypto: undefined | { - // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#browser_compatibility - getRandomValues?(data: Uint8Array): Uint8Array; - // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID#browser_compatibility - randomUUID?(): string; -}; - -export const generateUuid = (function (): () => string { - - // use `randomUUID` if possible - if (typeof crypto === 'object' && typeof crypto.randomUUID === 'function') { - return crypto.randomUUID.bind(crypto); - } - - // use `randomValues` if possible - let getRandomValues: (bucket: Uint8Array) => Uint8Array; - if (typeof crypto === 'object' && typeof crypto.getRandomValues === 'function') { - getRandomValues = crypto.getRandomValues.bind(crypto); - - } else { - getRandomValues = function (bucket: Uint8Array): Uint8Array { - for (let i = 0; i < bucket.length; i++) { - bucket[i] = Math.floor(Math.random() * 256); - } - return bucket; - }; - } - - // prep-work - const _data = new Uint8Array(16); - const _hex: string[] = []; - for (let i = 0; i < 256; i++) { - _hex.push(i.toString(16).padStart(2, '0')); - } - - // eslint-disable-next-line @typescript-eslint/no-shadow - return function generateUuid(): string { - // get data - getRandomValues(_data); - - // set version bits - _data[6] = (_data[6] & 0x0f) | 0x40; - _data[8] = (_data[8] & 0x3f) | 0x80; +export function generateUuid(): string { + return v4(); +} - // print as string - let i = 0; - let result = ''; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - return result; - }; -})(); +const NAMESPACE = '4c90ee4f-d952-44b1-83ca-f04121ab8e05'; +/** + * This function will hash the given value using SHA1. The result will be a uuid. + * @param value the string to hash + * @returns a uuid + */ +export function hashValue(value: string): string { + // as opposed to v4, v5 is deterministic and uses SHA1 hashing + return v5(value, NAMESPACE); +} diff --git a/packages/core/src/electron-node/token/electron-token-messaging-contribution.ts b/packages/core/src/electron-browser/electron-uri-handler.ts similarity index 50% rename from packages/core/src/electron-node/token/electron-token-messaging-contribution.ts rename to packages/core/src/electron-browser/electron-uri-handler.ts index 631ba2c99a2c6..c3fdd3a993dae 100644 --- a/packages/core/src/electron-node/token/electron-token-messaging-contribution.ts +++ b/packages/core/src/electron-browser/electron-uri-handler.ts @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright (C) 2020 Ericsson and others. +// 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 @@ -14,28 +14,29 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import * as http from 'http'; +import { FrontendApplicationContribution, OpenerService } from '../browser'; + import { injectable, inject } from 'inversify'; -import { MessagingContribution } from '../../node/messaging/messaging-contribution'; -import { ElectronTokenValidator } from './electron-token-validator'; +import { URI } from '../common'; -/** - * Override the browser MessagingContribution class to refuse connections that do not include a specific token. - * @deprecated since 1.8.0 - */ @injectable() -export class ElectronMessagingContribution extends MessagingContribution { - - @inject(ElectronTokenValidator) - protected readonly tokenValidator: ElectronTokenValidator; +export class ElectronUriHandlerContribution implements FrontendApplicationContribution { + @inject(OpenerService) + protected readonly openenerService: OpenerService; - /** - * Only allow token-bearers. - */ - protected override async allowConnect(request: http.IncomingMessage): Promise { - if (this.tokenValidator.allowRequest(request)) { - return super.allowConnect(request); - } - return false; + initialize(): void { + window.electronTheiaCore.setOpenUrlHandler(async url => { + const uri = new URI(url); + try { + const handler = await this.openenerService.getOpener(uri); + if (handler) { + await handler.open(uri); + return true; + } + } catch (e) { + // no handler + } + return false; + }); } } diff --git a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts index 8ed8b1cebd1a8..7633930144c03 100644 --- a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts +++ b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts @@ -104,11 +104,13 @@ export class ElectronContextMenuRenderer extends BrowserContextMenuRenderer { const menu = this.electronMenuFactory.createElectronContextMenu(menuPath, args, context, contextKeyService, skipSingleRootNode); const { x, y } = coordinateFromAnchor(anchor); + const windowName = options.context?.ownerDocument.defaultView?.Window.name; + const menuHandle = window.electronTheiaCore.popup(menu, x, y, () => { if (onHide) { onHide(); } - }); + }, windowName); // native context menu stops the event loop, so there is no keyboard events this.context.resetAltPressed(); return new ElectronContextMenuAccess(menuHandle); diff --git a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts index 162b4dca9235c..3925b56df82f3 100644 --- a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts +++ b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts @@ -74,39 +74,45 @@ export type ElectronMenuItemRole = ('undo' | 'redo' | 'cut' | 'copy' | 'paste' | @injectable() export class ElectronMainMenuFactory extends BrowserMainMenuFactory { - protected _menu?: MenuDto[]; - protected _toggledCommands: Set = new Set(); + protected menu?: MenuDto[]; + protected toggledCommands: Set = new Set(); @inject(PreferenceService) protected preferencesService: PreferenceService; + setMenuBar = debounce(() => this.doSetMenuBar(), 100); + @postConstruct() postConstruct(): void { - this.preferencesService.onPreferenceChanged( - debounce(e => { - if (e.preferenceName === 'window.menuBarVisibility') { - this.setMenuBar(); - } - if (this._menu) { - for (const cmd of this._toggledCommands) { - const menuItem = this.findMenuById(this._menu, cmd); - if (menuItem) { - menuItem.checked = this.commandRegistry.isToggled(cmd); - } - } - window.electronTheiaCore.setMenu(this._menu); - } - }, 10) - ); this.keybindingRegistry.onKeybindingsChanged(() => { this.setMenuBar(); }); + this.menuProvider.onDidChange(() => { + this.setMenuBar(); + }); + this.preferencesService.ready.then(() => { + this.preferencesService.onPreferenceChanged( + debounce(e => { + if (e.preferenceName === 'window.menuBarVisibility') { + this.doSetMenuBar(); + } + if (this.menu) { + for (const cmd of this.toggledCommands) { + const menuItem = this.findMenuById(this.menu, cmd); + if (menuItem && (!!menuItem.checked !== this.commandRegistry.isToggled(cmd))) { + menuItem.checked = !menuItem.checked; + } + } + window.electronTheiaCore.setMenu(this.menu); + } + }, 10) + ); + }); } - async setMenuBar(): Promise { - await this.preferencesService.ready; - const createdMenuBar = this.createElectronMenuBar(); - window.electronTheiaCore.setMenu(createdMenuBar); + doSetMenuBar(): void { + this.menu = this.createElectronMenuBar(); + window.electronTheiaCore.setMenu(this.menu); } createElectronMenuBar(): MenuDto[] | undefined { @@ -114,26 +120,25 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { const maxWidget = document.getElementsByClassName(MAXIMIZED_CLASS); if (preference === 'visible' || (preference === 'classic' && maxWidget.length === 0)) { const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); - this._menu = this.fillMenuTemplate([], menuModel, [], { honorDisabled: false, rootMenuPath: MAIN_MENU_BAR }); + const menu = this.fillMenuTemplate([], menuModel, [], { honorDisabled: false, rootMenuPath: MAIN_MENU_BAR }, false); if (isOSX) { - this._menu.unshift(this.createOSXMenu()); + menu.unshift(this.createOSXMenu()); } - return this._menu; + return menu; } - this._menu = undefined; - // eslint-disable-next-line no-null/no-null return undefined; } createElectronContextMenu(menuPath: MenuPath, args?: any[], context?: HTMLElement, contextKeyService?: ContextMatcher, skipSingleRootNode?: boolean): MenuDto[] { const menuModel = skipSingleRootNode ? this.menuProvider.removeSingleRootNode(this.menuProvider.getMenu(menuPath), menuPath) : this.menuProvider.getMenu(menuPath); - return this.fillMenuTemplate([], menuModel, args, { showDisabled: true, context, rootMenuPath: menuPath, contextKeyService }); + return this.fillMenuTemplate([], menuModel, args, { showDisabled: true, context, rootMenuPath: menuPath, contextKeyService }, true); } protected fillMenuTemplate(parentItems: MenuDto[], menu: MenuNode, args: unknown[] = [], - options: ElectronMenuOptions + options: ElectronMenuOptions, + skipRoot: boolean ): MenuDto[] { const showDisabled = options?.showDisabled !== false; const honorDisabled = options?.honorDisabled !== false; @@ -145,13 +150,13 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { } const children = CompoundMenuNode.getFlatChildren(menu.children); const myItems: MenuDto[] = []; - children.forEach(child => this.fillMenuTemplate(myItems, child, args, options)); + children.forEach(child => this.fillMenuTemplate(myItems, child, args, options, false)); if (myItems.length === 0) { return parentItems; } - if (role === CompoundMenuNodeRole.Submenu) { + if (!skipRoot && role === CompoundMenuNodeRole.Submenu) { parentItems.push({ label: menu.label, submenu: myItems }); - } else if (role === CompoundMenuNodeRole.Group && menu.id !== 'inline') { + } else { if (parentItems.length && parentItems[parentItems.length - 1].type !== 'separator') { parentItems.push({ type: 'separator' }); } @@ -204,7 +209,7 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { parentItems.push(menuItem); if (this.commandRegistry.getToggledHandler(commandId, ...args)) { - this._toggledCommands.add(commandId); + this.toggledCommands.add(commandId); } } return parentItems; @@ -269,11 +274,11 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { // We need to check if we can execute it. if (this.menuCommandExecutor.isEnabled(menuPath, cmd, ...args)) { await this.menuCommandExecutor.executeCommand(menuPath, cmd, ...args); - if (this._menu && this.menuCommandExecutor.isVisible(menuPath, cmd, ...args)) { - const item = this.findMenuById(this._menu, cmd); + if (this.menu && this.menuCommandExecutor.isVisible(menuPath, cmd, ...args)) { + const item = this.findMenuById(this.menu, cmd); if (item) { item.checked = this.menuCommandExecutor.isToggled(menuPath, cmd, ...args); - window.electronTheiaCore.setMenu(this._menu); + window.electronTheiaCore.setMenu(this.menu); } } } diff --git a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts index 199757021dfbd..9023dc2612021 100644 --- a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts +++ b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts @@ -18,7 +18,7 @@ import { inject, injectable, postConstruct } from 'inversify'; import { Command, CommandContribution, CommandRegistry, isOSX, isWindows, MenuModelRegistry, MenuContribution, Disposable, nls } from '../../common'; import { codicon, ConfirmDialog, KeybindingContribution, KeybindingRegistry, PreferenceScope, Widget, - FrontendApplication, FrontendApplicationContribution, CommonMenus, CommonCommands, Dialog, Message, ApplicationShell, + FrontendApplication, FrontendApplicationContribution, CommonMenus, CommonCommands, Dialog, Message, ApplicationShell, PreferenceService, animationFrame, } from '../../browser'; import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { FrontendApplicationStateService, FrontendApplicationState } from '../../browser/frontend-application-state'; @@ -29,7 +29,6 @@ import { WindowService } from '../../browser/window/window-service'; import { WindowTitleService } from '../../browser/window/window-title-service'; import '../../../src/electron-browser/menu/electron-menu-style.css'; -import { MenuDto } from '../../electron-common/electron-api'; import { ThemeService } from '../../browser/theming'; import { ThemeChangeEvent } from '../../common/theme'; @@ -203,7 +202,7 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme } } - protected setMenu(app: FrontendApplication, electronMenu: MenuDto[] | undefined = this.factory.createElectronMenuBar()): void { + protected setMenu(app: FrontendApplication): void { if (!isOSX) { this.hideTopPanel(app); if (this.titleBarStyle === 'custom' && !this.menuBar) { @@ -211,7 +210,7 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme return; } } - window.electronTheiaCore.setMenu(electronMenu); + this.factory.setMenuBar(); } protected createCustomTitleBar(app: FrontendApplication): void { @@ -441,6 +440,9 @@ export class CustomTitleWidget extends Widget { @inject(ApplicationShell) protected readonly applicationShell: ApplicationShell; + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + constructor() { super(); this.id = 'theia-custom-title'; @@ -452,6 +454,11 @@ export class CustomTitleWidget extends Widget { this.windowTitleService.onDidChangeTitle(title => { this.updateTitle(title); }); + this.preferenceService.onPreferenceChanged(e => { + if (e.preferenceName === 'window.menuBarVisibility') { + animationFrame().then(() => this.adjustTitleToCenter()); + } + }); } protected override onResize(msg: Widget.ResizeMessage): void { diff --git a/packages/core/src/electron-browser/messaging/electron-frontend-id-provider.ts b/packages/core/src/electron-browser/messaging/electron-frontend-id-provider.ts new file mode 100644 index 0000000000000..1e8c86a5667bd --- /dev/null +++ b/packages/core/src/electron-browser/messaging/electron-frontend-id-provider.ts @@ -0,0 +1,25 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { injectable } from 'inversify'; +import { FrontendIdProvider } from '../../browser/messaging/frontend-id-provider'; + +@injectable() +export class ElectronFrontendIdProvider implements FrontendIdProvider { + getId(): string { + return window.electronTheiaCore.WindowMetadata.webcontentId; + } +} diff --git a/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts b/packages/core/src/electron-browser/messaging/electron-ipc-connection-source.ts similarity index 61% rename from packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts rename to packages/core/src/electron-browser/messaging/electron-ipc-connection-source.ts index 1551e3ab9c8ed..5d02af95b7446 100644 --- a/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts +++ b/packages/core/src/electron-browser/messaging/electron-ipc-connection-source.ts @@ -16,32 +16,36 @@ import { injectable, interfaces } from 'inversify'; import { RpcProxy } from '../../common/messaging'; -import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; -import { AbstractChannel, Channel, WriteBuffer } from '../../common'; +import { AbstractChannel, Channel, Emitter, Event, MaybePromise, WriteBuffer } from '../../common'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer'; +import { ServiceConnectionProvider } from '../../browser/messaging/service-connection-provider'; +import { ConnectionSource } from '../../browser/messaging/connection-source'; +import { FrontendApplicationContribution } from '../../browser'; export interface ElectronIpcOptions { } +export const ElectronMainConnectionProvider = Symbol('ElectronMainConnectionProvider'); + /** * Connection provider between the Theia frontend and the electron-main process via IPC. */ -@injectable() -export class ElectronIpcConnectionProvider extends AbstractConnectionProvider { +export namespace ElectronIpcConnectionProvider { - static override createProxy(container: interfaces.Container, path: string, arg?: object): RpcProxy { - return container.get(ElectronIpcConnectionProvider).createProxy(path, arg); + export function createProxy(container: interfaces.Container, path: string, arg?: object): RpcProxy { + return container.get(ElectronMainConnectionProvider).createProxy(path, arg); } +} - constructor() { - super(); - this.initializeMultiplexer(); - } +@injectable() +export class ElectronIpcConnectionSource implements ConnectionSource, FrontendApplicationContribution { + protected readonly onConnectionDidOpenEmitter: Emitter = new Emitter(); + onConnectionDidOpen: Event = this.onConnectionDidOpenEmitter.event; - protected createMainChannel(): Channel { - return new ElectronIpcRendererChannel(); + onStart(): MaybePromise { + const channel = new ElectronIpcRendererChannel(); + this.onConnectionDidOpenEmitter.fire(channel); } - } export class ElectronIpcRendererChannel extends AbstractChannel { diff --git a/packages/core/src/electron-browser/messaging/electron-local-ws-connection-provider.ts b/packages/core/src/electron-browser/messaging/electron-local-ws-connection-source.ts similarity index 89% rename from packages/core/src/electron-browser/messaging/electron-local-ws-connection-provider.ts rename to packages/core/src/electron-browser/messaging/electron-local-ws-connection-source.ts index 01c7912cfe089..f62e740a02d48 100644 --- a/packages/core/src/electron-browser/messaging/electron-local-ws-connection-provider.ts +++ b/packages/core/src/electron-browser/messaging/electron-local-ws-connection-source.ts @@ -15,8 +15,8 @@ // ***************************************************************************** import { injectable } from 'inversify'; -import { WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider'; import { Endpoint } from '../../browser/endpoint'; +import { WebSocketConnectionSource } from '../../browser/messaging/ws-connection-source'; export function getLocalPort(): string | undefined { const params = new URLSearchParams(location.search); @@ -24,7 +24,7 @@ export function getLocalPort(): string | undefined { } @injectable() -export class ElectronLocalWebSocketConnectionProvider extends WebSocketConnectionProvider { +export class ElectronLocalWebSocketConnectionSource extends WebSocketConnectionSource { protected override createEndpoint(path: string): Endpoint { const localPort = getLocalPort(); diff --git a/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts b/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts index d62fc02a00607..7eef88d505e35 100644 --- a/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts +++ b/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts @@ -16,26 +16,63 @@ import { ContainerModule } from 'inversify'; import { FrontendApplicationContribution } from '../../browser/frontend-application-contribution'; -import { LocalWebSocketConnectionProvider, WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider'; -import { ElectronWebSocketConnectionProvider } from './electron-ws-connection-provider'; -import { ElectronIpcConnectionProvider } from './electron-ipc-connection-provider'; -import { ElectronLocalWebSocketConnectionProvider, getLocalPort } from './electron-local-ws-connection-provider'; +import { ElectronWebSocketConnectionSource } from './electron-ws-connection-source'; +import { ElectronIpcConnectionSource, ElectronMainConnectionProvider } from './electron-ipc-connection-source'; +import { ElectronLocalWebSocketConnectionSource, getLocalPort } from './electron-local-ws-connection-source'; +import { ElectronFrontendIdProvider } from './electron-frontend-id-provider'; +import { FrontendIdProvider } from '../../browser/messaging/frontend-id-provider'; +import { ConnectionSource } from '../../browser/messaging/connection-source'; +import { LocalConnectionProvider, RemoteConnectionProvider, ServiceConnectionProvider } from '../../browser/messaging/service-connection-provider'; +import { WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider'; +import { ConnectionCloseService, connectionCloseServicePath } from '../../common/messaging/connection-management'; +import { WebSocketConnectionSource } from '../../browser/messaging/ws-connection-source'; + +const backendServiceProvider = Symbol('backendServiceProvider2'); +const localServiceProvider = Symbol('localServiceProvider'); export const messagingFrontendModule = new ContainerModule(bind => { - bind(ElectronWebSocketConnectionProvider).toSelf().inSingletonScope(); - bind(FrontendApplicationContribution).toService(ElectronWebSocketConnectionProvider); - bind(WebSocketConnectionProvider).toService(ElectronWebSocketConnectionProvider); - bind(ElectronLocalWebSocketConnectionProvider).toSelf().inSingletonScope(); - bind(LocalWebSocketConnectionProvider).toDynamicValue(ctx => { + bind(ConnectionCloseService).toDynamicValue(ctx => WebSocketConnectionProvider.createProxy(ctx.container, connectionCloseServicePath)).inSingletonScope(); + bind(ElectronWebSocketConnectionSource).toSelf().inSingletonScope(); + bind(WebSocketConnectionSource).toService(ElectronWebSocketConnectionSource); + bind(ElectronIpcConnectionSource).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(ElectronIpcConnectionSource); + bind(ElectronLocalWebSocketConnectionSource).toSelf().inSingletonScope(); + bind(backendServiceProvider).toDynamicValue(ctx => { + const container = ctx.container.createChild(); + container.bind(ServiceConnectionProvider).toSelf().inSingletonScope(); + container.bind(ConnectionSource).toService(ElectronWebSocketConnectionSource); + return container.get(ServiceConnectionProvider); + }).inSingletonScope(); + + bind(localServiceProvider).toDynamicValue(ctx => { + const container = ctx.container.createChild(); + container.bind(ServiceConnectionProvider).toSelf().inSingletonScope(); + container.bind(ConnectionSource).toService(ElectronLocalWebSocketConnectionSource); + return container.get(ServiceConnectionProvider); + }).inSingletonScope(); + + bind(ElectronMainConnectionProvider).toDynamicValue(ctx => { + const container = ctx.container.createChild(); + container.bind(ServiceConnectionProvider).toSelf().inSingletonScope(); + container.bind(ConnectionSource).toService(ElectronIpcConnectionSource); + return container.get(ServiceConnectionProvider); + }).inSingletonScope(); + + bind(LocalConnectionProvider).toDynamicValue(ctx => { const localPort = getLocalPort(); if (localPort) { // Return new web socket provider that connects to local app - return ctx.container.get(ElectronLocalWebSocketConnectionProvider); + return ctx.container.get(localServiceProvider); } else { // Return the usual web socket provider that already established its connection // That way we don't create a second socket connection - return ctx.container.get(WebSocketConnectionProvider); + return ctx.container.get(backendServiceProvider); } }).inSingletonScope(); - bind(ElectronIpcConnectionProvider).toSelf().inSingletonScope(); + bind(RemoteConnectionProvider).toService(backendServiceProvider); + + bind(FrontendApplicationContribution).toService(ElectronWebSocketConnectionSource); + bind(ElectronFrontendIdProvider).toSelf().inSingletonScope(); + bind(FrontendIdProvider).toService(ElectronFrontendIdProvider); + bind(WebSocketConnectionProvider).toSelf().inSingletonScope(); }); diff --git a/packages/core/src/electron-browser/messaging/electron-ws-connection-provider.ts b/packages/core/src/electron-browser/messaging/electron-ws-connection-source.ts similarity index 67% rename from packages/core/src/electron-browser/messaging/electron-ws-connection-provider.ts rename to packages/core/src/electron-browser/messaging/electron-ws-connection-source.ts index a9d0f161ab0df..258bb148d5ea6 100644 --- a/packages/core/src/electron-browser/messaging/electron-ws-connection-provider.ts +++ b/packages/core/src/electron-browser/messaging/electron-ws-connection-source.ts @@ -15,9 +15,8 @@ // ***************************************************************************** import { injectable } from 'inversify'; -import { WebSocketConnectionProvider, WebSocketOptions } from '../../browser/messaging/ws-connection-provider'; import { FrontendApplicationContribution } from '../../browser/frontend-application-contribution'; -import { Channel } from '../../common'; +import { WebSocketConnectionSource } from '../../browser/messaging/ws-connection-source'; /** * Customized connection provider between the frontend and the backend in electron environment. @@ -25,25 +24,15 @@ import { Channel } from '../../common'; * once the electron-browser window is refreshed. Otherwise, backend resources are not disposed. */ @injectable() -export class ElectronWebSocketConnectionProvider extends WebSocketConnectionProvider implements FrontendApplicationContribution { - - /** - * Do not try to reconnect when the frontend application is stopping. The browser is navigating away from this page. - */ - protected stopping = false; +export class ElectronWebSocketConnectionSource extends WebSocketConnectionSource implements FrontendApplicationContribution { + constructor() { + super(); + } onStop(): void { - this.stopping = true; // Manually close the websocket connections `onStop`. Otherwise, the channels will be closed with 30 sec (`MessagingContribution#checkAliveTimeout`) delay. // https://github.com/eclipse-theia/theia/issues/6499 // `1001` indicates that an endpoint is "going away", such as a server going down or a browser having navigated away from a page. - this.channelMultiplexer?.onUnderlyingChannelClose({ reason: 'The frontend is "going away"', code: 1001 }); + this.socket.close(); } - - override async openChannel(path: string, handler: (channel: Channel) => void, options?: WebSocketOptions): Promise { - if (!this.stopping) { - super.openChannel(path, handler, options); - } - } - } diff --git a/packages/core/src/electron-browser/preload.ts b/packages/core/src/electron-browser/preload.ts index 1ce37cc52b633..57755899242d8 100644 --- a/packages/core/src/electron-browser/preload.ts +++ b/packages/core/src/electron-browser/preload.ts @@ -25,7 +25,9 @@ import { CHANNEL_ON_WINDOW_EVENT, CHANNEL_GET_ZOOM_LEVEL, CHANNEL_SET_ZOOM_LEVEL, CHANNEL_IS_FULL_SCREENABLE, CHANNEL_TOGGLE_FULL_SCREEN, CHANNEL_IS_FULL_SCREEN, CHANNEL_SET_MENU_BAR_VISIBLE, CHANNEL_REQUEST_CLOSE, CHANNEL_SET_TITLE_STYLE, CHANNEL_RESTART, CHANNEL_REQUEST_RELOAD, CHANNEL_APP_STATE_CHANGED, CHANNEL_SHOW_ITEM_IN_FOLDER, CHANNEL_READ_CLIPBOARD, CHANNEL_WRITE_CLIPBOARD, - CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto, CHANNEL_REQUEST_SECONDARY_CLOSE, CHANNEL_SET_BACKGROUND_COLOR + CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto, CHANNEL_REQUEST_SECONDARY_CLOSE, CHANNEL_SET_BACKGROUND_COLOR, + CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE, CHANNEL_OPEN_WITH_SYSTEM_APP, + CHANNEL_OPEN_URL } from '../electron-common/electron-api'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -33,10 +35,20 @@ const { ipcRenderer, contextBridge } = require('electron'); // a map of menuId => map handler> const commandHandlers = new Map void>>(); -let nextHandlerId = 0; -const mainMenuId = 0; +let nextHandlerId = 1; +const mainMenuId = 1; let nextMenuId = mainMenuId + 1; +let openUrlHandler: ((url: string) => Promise) | undefined; + +ipcRenderer.on(CHANNEL_OPEN_URL, async (event: Electron.IpcRendererEvent, url: string, replyChannel: string) => { + if (openUrlHandler) { + event.sender.send(replyChannel, await openUrlHandler(url)); + } else { + event.sender.send(replyChannel, false); + } +}); + function convertMenu(menu: MenuDto[] | undefined, handlerMap: Map void>): InternalMenuDto[] | undefined { if (!menu) { return undefined; @@ -65,6 +77,7 @@ function convertMenu(menu: MenuDto[] | undefined, handlerMap: Map } const api: TheiaCoreAPI = { + WindowMetadata: { webcontentId: 'none' }, setMenuBarVisible: (visible: boolean, windowName?: string) => ipcRenderer.send(CHANNEL_SET_MENU_BAR_VISIBLE, visible, windowName), setMenu: (menu: MenuDto[] | undefined) => { commandHandlers.delete(mainMenuId); @@ -73,17 +86,20 @@ const api: TheiaCoreAPI = { ipcRenderer.send(CHANNEL_SET_MENU, mainMenuId, convertMenu(menu, handlers)); }, getSecurityToken: () => ipcRenderer.sendSync(CHANNEL_GET_SECURITY_TOKEN), - focusWindow: (name: string) => ipcRenderer.send(CHANNEL_FOCUS_WINDOW, name), + focusWindow: (name?: string) => ipcRenderer.send(CHANNEL_FOCUS_WINDOW, name), showItemInFolder: fsPath => { ipcRenderer.send(CHANNEL_SHOW_ITEM_IN_FOLDER, fsPath); }, + openWithSystemApp: location => { + ipcRenderer.send(CHANNEL_OPEN_WITH_SYSTEM_APP, location); + }, attachSecurityToken: (endpoint: string) => ipcRenderer.invoke(CHANNEL_ATTACH_SECURITY_TOKEN, endpoint), - popup: async function (menu: MenuDto[], x: number, y: number, onClosed: () => void): Promise { + popup: async function (menu: MenuDto[], x: number, y: number, onClosed: () => void, windowName?: string): Promise { const menuId = nextMenuId++; const handlers = new Map void>(); commandHandlers.set(menuId, handlers); - const handle = await ipcRenderer.invoke(CHANNEL_OPEN_POPUP, menuId, convertMenu(menu, handlers), x, y); + const handle = await ipcRenderer.invoke(CHANNEL_OPEN_POPUP, menuId, convertMenu(menu, handlers), x, y, windowName); const closeListener = () => { ipcRenderer.removeListener(CHANNEL_ON_CLOSE_POPUP, closeListener); commandHandlers.delete(menuId); @@ -119,6 +135,21 @@ const api: TheiaCoreAPI = { close: function (): void { ipcRenderer.send(CHANNEL_CLOSE); }, + + onAboutToClose(handler: () => void): Disposable { + const h = (event: Electron.IpcRendererEvent, replyChannel: string) => { + handler(); + event.sender.send(replyChannel); + }; + + ipcRenderer.on(CHANNEL_ABOUT_TO_CLOSE, h); + return Disposable.create(() => ipcRenderer.off(CHANNEL_ABOUT_TO_CLOSE, h)); + }, + + setOpenUrlHandler(handler: (url: string) => Promise): void { + openUrlHandler = handler; + }, + onWindowEvent: function (event: WindowEvent, handler: () => void): Disposable { const h = (_event: unknown, evt: WindowEvent) => { if (event === evt) { @@ -183,7 +214,7 @@ const api: TheiaCoreAPI = { ipcRenderer.send(CHANNEL_TOGGLE_FULL_SCREEN); }, - requestReload: () => ipcRenderer.send(CHANNEL_REQUEST_RELOAD), + requestReload: (newUrl?: string) => ipcRenderer.send(CHANNEL_REQUEST_RELOAD, newUrl), restart: () => ipcRenderer.send(CHANNEL_RESTART), applicationStateChanged: state => { @@ -227,6 +258,7 @@ export function preload(): void { } } }); + api.WindowMetadata.webcontentId = ipcRenderer.sendSync(CHANNEL_WC_METADATA); contextBridge.exposeInMainWorld('electronTheiaCore', api); } diff --git a/packages/core/src/electron-browser/window/electron-secondary-window-service.ts b/packages/core/src/electron-browser/window/electron-secondary-window-service.ts index a5bd35cdca469..c9ed6079a4931 100644 --- a/packages/core/src/electron-browser/window/electron-secondary-window-service.ts +++ b/packages/core/src/electron-browser/window/electron-secondary-window-service.ts @@ -24,16 +24,12 @@ export class ElectronSecondaryWindowService extends DefaultSecondaryWindowServic window.electronTheiaCore.focusWindow(win.name); } - protected override doCreateSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): Window | undefined { - const w = super.doCreateSecondaryWindow(widget, shell); - if (w) { - window.electronTheiaCore.setMenuBarVisible(false, w.name); - window.electronTheiaCore.setSecondaryWindowCloseRequestHandler(w.name, () => this.canClose(widget, shell)); - } - return w; + protected override windowCreated(newWindow: Window, widget: ExtractableWidget, shell: ApplicationShell): void { + window.electronTheiaCore.setMenuBarVisible(false, newWindow.name); + window.electronTheiaCore.setSecondaryWindowCloseRequestHandler(newWindow.name, () => this.canClose(widget, shell)); } private async canClose(widget: ExtractableWidget, shell: ApplicationShell): Promise { - await shell.closeWidget(widget.id, undefined); + await shell.closeWidget(widget.id); return widget.isDisposed; } } diff --git a/packages/core/src/electron-browser/window/electron-window-module.ts b/packages/core/src/electron-browser/window/electron-window-module.ts index 360ba55aa36e4..b4edcee810a10 100644 --- a/packages/core/src/electron-browser/window/electron-window-module.ts +++ b/packages/core/src/electron-browser/window/electron-window-module.ts @@ -15,18 +15,21 @@ // ***************************************************************************** import { ContainerModule } from 'inversify'; -import { WindowService } from '../../browser/window/window-service'; -import { ElectronWindowService } from './electron-window-service'; -import { FrontendApplicationContribution } from '../../browser/frontend-application-contribution'; -import { ElectronClipboardService } from '../electron-clipboard-service'; +import { OpenHandler } from '../../browser'; import { ClipboardService } from '../../browser/clipboard-service'; -import { ElectronMainWindowService, electronMainWindowServicePath } from '../../electron-common/electron-main-window-service'; -import { ElectronIpcConnectionProvider } from '../messaging/electron-ipc-connection-provider'; -import { bindWindowPreferences } from './electron-window-preferences'; +import { FrontendApplicationContribution } from '../../browser/frontend-application-contribution'; import { FrontendApplicationStateService } from '../../browser/frontend-application-state'; +import { SecondaryWindowService } from '../../browser/window/secondary-window-service'; +import { WindowService } from '../../browser/window/window-service'; +import { ElectronMainWindowService, electronMainWindowServicePath } from '../../electron-common/electron-main-window-service'; +import { ElectronClipboardService } from '../electron-clipboard-service'; +import { ElectronIpcConnectionProvider } from '../messaging/electron-ipc-connection-source'; import { ElectronFrontendApplicationStateService } from './electron-frontend-application-state'; import { ElectronSecondaryWindowService } from './electron-secondary-window-service'; -import { SecondaryWindowService } from '../../browser/window/secondary-window-service'; +import { bindWindowPreferences } from './electron-window-preferences'; +import { ElectronWindowService } from './electron-window-service'; +import { ExternalAppOpenHandler } from './external-app-open-handler'; +import { ElectronUriHandlerContribution } from '../electron-uri-handler'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ElectronMainWindowService).toDynamicValue(context => @@ -35,7 +38,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bindWindowPreferences(bind); bind(WindowService).to(ElectronWindowService).inSingletonScope(); bind(FrontendApplicationContribution).toService(WindowService); + bind(ElectronUriHandlerContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(ElectronUriHandlerContribution); bind(ClipboardService).to(ElectronClipboardService).inSingletonScope(); rebind(FrontendApplicationStateService).to(ElectronFrontendApplicationStateService).inSingletonScope(); bind(SecondaryWindowService).to(ElectronSecondaryWindowService).inSingletonScope(); + bind(ExternalAppOpenHandler).toSelf().inSingletonScope(); + bind(OpenHandler).toService(ExternalAppOpenHandler); }); diff --git a/packages/core/src/electron-browser/window/electron-window-preferences.ts b/packages/core/src/electron-browser/window/electron-window-preferences.ts index 1cc08cc11e5d7..d8addf1f7fb8c 100644 --- a/packages/core/src/electron-browser/window/electron-window-preferences.ts +++ b/packages/core/src/electron-browser/window/electron-window-preferences.ts @@ -38,7 +38,7 @@ export const electronWindowPreferencesSchema: PreferenceSchema = { 'maximum': ZoomLevel.MAX, 'scope': 'application', // eslint-disable-next-line max-len - 'description': nls.localizeByDefault('Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.') + 'description': nls.localizeByDefault("Adjust the default zoom level for all windows. Each increment above `0` (e.g. `1`) or below (e.g. `-1`) represents zooming `20%` larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity. See {0} for configuring if the 'Zoom In' and 'Zoom Out' commands apply the zoom level to all windows or only the active window.") }, 'window.titleBarStyle': { type: 'string', diff --git a/packages/core/src/electron-browser/window/electron-window-service.ts b/packages/core/src/electron-browser/window/electron-window-service.ts index 44442ed5d4d21..699b8373b4db7 100644 --- a/packages/core/src/electron-browser/window/electron-window-service.ts +++ b/packages/core/src/electron-browser/window/electron-window-service.ts @@ -19,6 +19,9 @@ import { NewWindowOptions, WindowSearchParams } from '../../common/window'; import { DefaultWindowService } from '../../browser/window/default-window-service'; import { ElectronMainWindowService } from '../../electron-common/electron-main-window-service'; import { ElectronWindowPreferences } from './electron-window-preferences'; +import { ConnectionCloseService } from '../../common/messaging/connection-management'; +import { FrontendIdProvider } from '../../browser/messaging/frontend-id-provider'; +import { WindowReloadOptions } from '../../browser/window/window-service'; @injectable() export class ElectronWindowService extends DefaultWindowService { @@ -33,12 +36,18 @@ export class ElectronWindowService extends DefaultWindowService { */ protected closeOnUnload: boolean = false; + @inject(FrontendIdProvider) + protected readonly frontendIdProvider: FrontendIdProvider; + @inject(ElectronMainWindowService) protected readonly delegate: ElectronMainWindowService; @inject(ElectronWindowPreferences) protected readonly electronWindowPreferences: ElectronWindowPreferences; + @inject(ConnectionCloseService) + protected readonly connectionCloseService: ConnectionCloseService; + override openNewWindow(url: string, { external }: NewWindowOptions = {}): undefined { this.delegate.openNewWindow(url, { external }); return undefined; @@ -48,6 +57,9 @@ export class ElectronWindowService extends DefaultWindowService { this.delegate.openNewDefaultWindow(params); } + override focus(): void { + window.electronTheiaCore.focusWindow(); + } @postConstruct() protected init(): void { // Update the default zoom level on startup when the preferences event is fired. @@ -56,6 +68,9 @@ export class ElectronWindowService extends DefaultWindowService { this.updateWindowZoomLevel(); } }); + window.electronTheiaCore.onAboutToClose(() => { + this.connectionCloseService.markForClose(this.frontendIdProvider.getId()); + }); } protected override registerUnloadListeners(): void { @@ -75,12 +90,20 @@ export class ElectronWindowService extends DefaultWindowService { } } - override reload(params?: WindowSearchParams): void { + override reload(params?: WindowReloadOptions): void { if (params) { - const query = Object.entries(params).map(([name, value]) => `${name}=${value}`).join('&'); - location.search = query; + const newLocation = new URL(location.href); + if (params.search) { + const query = Object.entries(params.search).map(([name, value]) => `${name}=${value}`).join('&'); + newLocation.search = query; + } + if (params.hash) { + newLocation.hash = '#' + params.hash; + } + window.electronTheiaCore.requestReload(newLocation.toString()); } else { window.electronTheiaCore.requestReload(); } } } + diff --git a/packages/core/src/electron-browser/window/external-app-open-handler.ts b/packages/core/src/electron-browser/window/external-app-open-handler.ts new file mode 100644 index 0000000000000..26f86c64be266 --- /dev/null +++ b/packages/core/src/electron-browser/window/external-app-open-handler.ts @@ -0,0 +1,42 @@ +// ***************************************************************************** +// 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 { injectable } from 'inversify'; +import { OpenHandler } from '../../browser/opener-service'; +import URI from '../../common/uri'; +import { HttpOpenHandler } from '../../browser/http-open-handler'; + +export interface ExternalAppOpenHandlerOptions { + openExternalApp?: boolean +} + +@injectable() +export class ExternalAppOpenHandler implements OpenHandler { + + static readonly PRIORITY: number = HttpOpenHandler.PRIORITY + 100; + readonly id = 'external-app'; + + canHandle(uri: URI, options?: ExternalAppOpenHandlerOptions): number { + return (options && options.openExternalApp) ? ExternalAppOpenHandler.PRIORITY : -1; + } + + async open(uri: URI): Promise { + // For files 'file:' scheme, system accepts only the path. + // For other protocols e.g. 'vscode:' we use the full URI to propagate target app information. + window.electronTheiaCore.openWithSystemApp(uri.toString(true)); + return undefined; + } +} diff --git a/packages/core/src/electron-common/electron-api.ts b/packages/core/src/electron-common/electron-api.ts index 9007a41e747e5..79035b3e00d37 100644 --- a/packages/core/src/electron-common/electron-api.ts +++ b/packages/core/src/electron-common/electron-api.ts @@ -41,19 +41,27 @@ export type InternalMenuDto = Omit & { export type WindowEvent = 'maximize' | 'unmaximize' | 'focus'; export interface TheiaCoreAPI { + WindowMetadata: { + webcontentId: string; + } getSecurityToken: () => string; attachSecurityToken: (endpoint: string) => Promise; setMenuBarVisible(visible: boolean, windowName?: string): void; setMenu(menu: MenuDto[] | undefined): void; - popup(menu: MenuDto[], x: number, y: number, onClosed: () => void): Promise; + popup(menu: MenuDto[], x: number, y: number, onClosed: () => void, windowName?: string): Promise; closePopup(handle: number): void; - focusWindow(name: string): void; + focusWindow(name?: string): void; showItemInFolder(fsPath: string): void; + /** + * @param location The location to open with the system app. This can be a file path or a URL. + */ + openWithSystemApp(location: string): void; + getTitleBarStyleAtStartup(): Promise; setTitleBarStyle(style: string): void; setBackgroundColor(backgroundColor: string): void; @@ -63,8 +71,11 @@ export interface TheiaCoreAPI { unMaximize(): void; close(): void; onWindowEvent(event: WindowEvent, handler: () => void): Disposable; + onAboutToClose(handler: () => void): Disposable; setCloseRequestHandler(handler: (reason: StopReason) => Promise): void; + setOpenUrlHandler(handler: (url: string) => Promise): void; + setSecondaryWindowCloseRequestHandler(windowName: string, handler: () => Promise): void; toggleDevTools(): void; @@ -75,7 +86,7 @@ export interface TheiaCoreAPI { isFullScreen(): boolean; // TODO: this should really be async, since it blocks the renderer process toggleFullScreen(): void; - requestReload(): void; + requestReload(newUrl?: string): void; restart(): void; applicationStateChanged(state: FrontendApplicationState): void; @@ -96,6 +107,7 @@ declare global { } } +export const CHANNEL_WC_METADATA = 'WebContentMetadata'; export const CHANNEL_SET_MENU = 'SetMenu'; export const CHANNEL_SET_MENU_BAR_VISIBLE = 'SetMenuBarVisible'; export const CHANNEL_INVOKE_MENU = 'InvokeMenu'; @@ -107,6 +119,7 @@ export const CHANNEL_FOCUS_WINDOW = 'FocusWindow'; export const CHANNEL_SHOW_OPEN = 'ShowOpenDialog'; export const CHANNEL_SHOW_SAVE = 'ShowSaveDialog'; export const CHANNEL_SHOW_ITEM_IN_FOLDER = 'ShowItemInFolder'; +export const CHANNEL_OPEN_WITH_SYSTEM_APP = 'OpenWithSystemApp'; export const CHANNEL_ATTACH_SECURITY_TOKEN = 'AttachSecurityToken'; export const CHANNEL_GET_TITLE_STYLE_AT_STARTUP = 'GetTitleStyleAtStartup'; @@ -117,6 +130,9 @@ export const CHANNEL_MINIMIZE = 'Minimize'; export const CHANNEL_MAXIMIZE = 'Maximize'; export const CHANNEL_IS_MAXIMIZED = 'IsMaximized'; +export const CHANNEL_ABOUT_TO_CLOSE = 'AboutToClose'; +export const CHANNEL_OPEN_URL = 'OpenUrl'; + export const CHANNEL_UNMAXIMIZE = 'UnMaximize'; export const CHANNEL_ON_WINDOW_EVENT = 'OnWindowEvent'; export const CHANNEL_TOGGLE_DEVTOOLS = 'ToggleDevtools'; diff --git a/packages/core/src/electron-main/electron-api-main.ts b/packages/core/src/electron-main/electron-api-main.ts index 4eeff065ad556..b96942633e0e4 100644 --- a/packages/core/src/electron-main/electron-api-main.ts +++ b/packages/core/src/electron-main/electron-api-main.ts @@ -51,7 +51,11 @@ import { CHANNEL_TOGGLE_FULL_SCREEN, CHANNEL_IS_MAXIMIZED, CHANNEL_REQUEST_SECONDARY_CLOSE, - CHANNEL_SET_BACKGROUND_COLOR + CHANNEL_SET_BACKGROUND_COLOR, + CHANNEL_WC_METADATA, + CHANNEL_ABOUT_TO_CLOSE, + CHANNEL_OPEN_WITH_SYSTEM_APP, + CHANNEL_OPEN_URL } from '../electron-common/electron-api'; import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application'; import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common'; @@ -65,6 +69,10 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { protected readonly openPopups = new Map(); onStart(application: ElectronMainApplication): MaybePromise { + ipcMain.on(CHANNEL_WC_METADATA, event => { + event.returnValue = event.sender.id.toString(); + }); + // electron security token ipcMain.on(CHANNEL_GET_SECURITY_TOKEN, event => { event.returnValue = this.electronSecurityToken.value; @@ -109,7 +117,7 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { }); // popup menu - ipcMain.handle(CHANNEL_OPEN_POPUP, (event, menuId, menu, x, y) => { + ipcMain.handle(CHANNEL_OPEN_POPUP, (event, menuId, menu, x, y, windowName?: string) => { const zoom = event.sender.getZoomFactor(); // TODO: Remove the offset once Electron fixes https://github.com/electron/electron/issues/31641 const offset = process.platform === 'win32' ? 0 : 2; @@ -118,7 +126,14 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { y = Math.round(y * zoom) + offset; const popup = Menu.buildFromTemplate(this.fromMenuDto(event.sender, menuId, menu)); this.openPopups.set(menuId, popup); + let electronWindow: BrowserWindow | undefined; + if (windowName) { + electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName); + } else { + electronWindow = BrowserWindow.fromWebContents(event.sender) || undefined; + } popup.popup({ + window: electronWindow, callback: () => { this.openPopups.delete(menuId); event.sender.send(CHANNEL_ON_CLOSE_POPUP, menuId); @@ -134,7 +149,9 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { // focus windows for secondary window support ipcMain.on(CHANNEL_FOCUS_WINDOW, (event, windowName) => { - const electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName); + const electronWindow = windowName + ? BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName) + : BrowserWindow.fromWebContents(event.sender); if (electronWindow) { if (electronWindow.isMinimized()) { electronWindow.restore(); @@ -149,6 +166,10 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { shell.showItemInFolder(fsPath); }); + ipcMain.on(CHANNEL_OPEN_WITH_SYSTEM_APP, (event, uri) => { + shell.openExternal(uri); + }); + ipcMain.handle(CHANNEL_GET_TITLE_STYLE_AT_STARTUP, event => application.getTitleBarStyleAtStartup(event.sender)); ipcMain.on(CHANNEL_SET_TITLE_STYLE, (event, style) => application.setTitleBarStyle(event.sender, style)); @@ -221,9 +242,20 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { }); } + private isASCI(accelerator: string | undefined): boolean { + if (typeof accelerator !== 'string') { + return false; + } + for (let i = 0; i < accelerator.length; i++) { + if (accelerator.charCodeAt(i) > 127) { + return false; + } + } + return true; + } + fromMenuDto(sender: WebContents, menuId: number, menuDto: InternalMenuDto[]): MenuItemConstructorOptions[] { return menuDto.map(dto => { - const result: MenuItemConstructorOptions = { id: dto.id, label: dto.label, @@ -232,7 +264,7 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { enabled: dto.enabled, visible: dto.visible, role: dto.role, - accelerator: dto.accelerator + accelerator: this.isASCI(dto.accelerator) ? dto.accelerator : undefined }; if (dto.submenu) { result.submenu = this.fromMenuDto(sender, menuId, dto.submenu); @@ -254,6 +286,33 @@ export namespace TheiaRendererAPI { wc.send(CHANNEL_ON_WINDOW_EVENT, event); } + export function openUrl(wc: WebContents, url: string): Promise { + return new Promise(resolve => { + const channelNr = nextReplyChannel++; + const replyChannel = `openUrl${channelNr}`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const l = createDisposableListener(ipcMain, replyChannel, (e, args: any[]) => { + l.dispose(); + resolve(args[0]); + }); + + wc.send(CHANNEL_OPEN_URL, url, replyChannel); + }); + } + + export function sendAboutToClose(wc: WebContents): Promise { + return new Promise(resolve => { + const channelNr = nextReplyChannel++; + const replyChannel = `aboutToClose${channelNr}`; + const l = createDisposableListener(ipcMain, replyChannel, e => { + l.dispose(); + resolve(); + }); + + wc.send(CHANNEL_ABOUT_TO_CLOSE, replyChannel); + }); + } + export function requestClose(wc: WebContents, stopReason: StopReason): Promise { const channelNr = nextReplyChannel++; const confirmChannel = `confirm-${channelNr}`; diff --git a/packages/core/src/electron-main/electron-main-application-module.ts b/packages/core/src/electron-main/electron-main-application-module.ts index 0c789b617d675..a512efb722012 100644 --- a/packages/core/src/electron-main/electron-main-application-module.ts +++ b/packages/core/src/electron-main/electron-main-application-module.ts @@ -15,27 +15,28 @@ // ***************************************************************************** import { ContainerModule } from 'inversify'; -import { v4 } from 'uuid'; +import { generateUuid } from '../common/uuid'; import { bindContributionProvider } from '../common/contribution-provider'; import { RpcConnectionHandler } from '../common/messaging/proxy-factory'; import { ElectronSecurityToken } from '../electron-common/electron-token'; import { ElectronMainWindowService, electronMainWindowServicePath } from '../electron-common/electron-main-window-service'; import { ElectronMainApplication, ElectronMainApplicationContribution, ElectronMainProcessArgv } from './electron-main-application'; import { ElectronMainWindowServiceImpl } from './electron-main-window-service-impl'; -import { ElectronMessagingContribution } from './messaging/electron-messaging-contribution'; -import { ElectronMessagingService } from './messaging/electron-messaging-service'; -import { ElectronConnectionHandler } from '../electron-common/messaging/electron-connection-handler'; -import { ElectronSecurityTokenService } from './electron-security-token-service'; import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory, WindowApplicationConfig } from './theia-electron-window'; import { TheiaMainApi } from './electron-api-main'; +import { ElectronMessagingContribution } from './messaging/electron-messaging-contribution'; +import { ElectronSecurityTokenService } from './electron-security-token-service'; +import { ElectronMessagingService } from './messaging/electron-messaging-service'; +import { ElectronConnectionHandler } from './messaging/electron-connection-handler'; -const electronSecurityToken: ElectronSecurityToken = { value: v4() }; +const electronSecurityToken: ElectronSecurityToken = { value: generateUuid() }; // eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any)[ElectronSecurityToken] = electronSecurityToken; export default new ContainerModule(bind => { bind(ElectronMainApplication).toSelf().inSingletonScope(); bind(ElectronMessagingContribution).toSelf().inSingletonScope(); + bind(ElectronMainApplicationContribution).toService(ElectronMessagingContribution); bind(ElectronSecurityToken).toConstantValue(electronSecurityToken); bind(ElectronSecurityTokenService).toSelf().inSingletonScope(); @@ -43,7 +44,6 @@ export default new ContainerModule(bind => { bindContributionProvider(bind, ElectronMessagingService.Contribution); bindContributionProvider(bind, ElectronMainApplicationContribution); - bind(ElectronMainApplicationContribution).toService(ElectronMessagingContribution); bind(TheiaMainApi).toSelf().inSingletonScope(); bind(ElectronMainApplicationContribution).toService(TheiaMainApi); diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index 3a67acd71ff1b..b464c916a84d9 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -15,23 +15,26 @@ // ***************************************************************************** import { inject, injectable, named } from 'inversify'; -import { screen, app, BrowserWindow, WebContents, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage, nativeTheme } from '../../electron-shared/electron'; +import { + screen, app, BrowserWindow, WebContents, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage, + nativeTheme, shell, dialog +} from '../../electron-shared/electron'; import * as path from 'path'; import { Argv } from 'yargs'; import { AddressInfo } from 'net'; import { promises as fs } from 'fs'; import { existsSync, mkdirSync } from 'fs-extra'; import { fork, ForkOptions } from 'child_process'; -import { DefaultTheme, FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; +import { DefaultTheme, ElectronFrontendApplicationConfig, FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; import URI from '../common/uri'; -import { FileUri } from '../node/file-uri'; -import { Deferred } from '../common/promise-util'; +import { FileUri } from '../common/file-uri'; +import { Deferred, timeout } from '../common/promise-util'; import { MaybePromise } from '../common/types'; import { ContributionProvider } from '../common/contribution-provider'; import { ElectronSecurityTokenService } from './electron-security-token-service'; import { ElectronSecurityToken } from '../electron-common/electron-token'; import Storage = require('electron-store'); -import { Disposable, DisposableCollection, isOSX, isWindows } from '../common'; +import { CancellationTokenSource, Disposable, DisposableCollection, Path, isOSX, isWindows } from '../common'; import { DEFAULT_WINDOW_HASH, WindowSearchParams } from '../common/window'; import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory } from './theia-electron-window'; import { ElectronMainApplicationGlobals } from './electron-main-constants'; @@ -54,19 +57,13 @@ export interface ElectronMainCommandOptions { */ readonly file?: string; -} + readonly cwd: string; -/** - * Fields related to a launch event. - * - * This kind of event is triggered in two different contexts: - * 1. The app is launched for the first time, `secondInstance` is false. - * 2. The app is already running but user relaunches it, `secondInstance` is true. - */ -export interface ElectronMainExecutionParams { + /** + * If the app is launched for the first time, `secondInstance` is false. + * If the app is already running but user relaunches it, `secondInstance` is true. + */ readonly secondInstance: boolean; - readonly argv: string[]; - readonly cwd: string; } /** @@ -120,13 +117,13 @@ export class ElectronMainProcessArgv { return 1; } - protected get isBundledElectronApp(): boolean { + get isBundledElectronApp(): boolean { // process.defaultApp is either set by electron in an electron unbundled app, or undefined // see https://github.com/electron/electron/blob/master/docs/api/process.md#processdefaultapp-readonly return this.isElectronApp && !(process as ElectronMainProcessArgv.ElectronMainProcess).defaultApp; } - protected get isElectronApp(): boolean { + get isElectronApp(): boolean { // process.versions.electron is either set by electron, or undefined // see https://github.com/electron/electron/blob/master/docs/api/process.md#processversionselectron-readonly return !!(process as ElectronMainProcessArgv.ElectronMainProcess).versions.electron; @@ -186,8 +183,10 @@ export class ElectronMainApplication { protected customBackgroundColor?: string; protected didUseNativeWindowFrameOnStart = new Map(); protected windows = new Map(); + protected activeWindowStack: number[] = []; protected restarting = false; + /** Used to temporarily store the reference to an early created main window */ protected initialWindow?: BrowserWindow; get config(): FrontendApplicationConfig { @@ -212,21 +211,39 @@ export class ElectronMainApplication { } async start(config: FrontendApplicationConfig): Promise { - const args = this.processArgv.getProcessArgvWithoutBin(process.argv); - this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native'; - this._config = config; - this.hookApplicationEvents(); - this.showInitialWindow(); - const port = await this.startBackend(); - this._backendPort.resolve(port); - await app.whenReady(); - await this.attachElectronSecurityToken(port); - await this.startContributions(); - await this.launch({ - secondInstance: false, - argv: args, - cwd: process.cwd() - }); + const argv = this.processArgv.getProcessArgvWithoutBin(process.argv); + createYargs(argv, process.cwd()) + .help(false) + .command('$0 [file]', false, + cmd => cmd + .option('electronUserData', { + type: 'string', + describe: 'The area where the electron main process puts its data' + }) + .positional('file', { type: 'string' }), + async args => { + if (args.electronUserData) { + console.info(`using electron user data area : '${args.electronUserData}'`); + await fs.mkdir(args.electronUserData, { recursive: true }); + app.setPath('userData', args.electronUserData); + } + this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native'; + this._config = config; + this.hookApplicationEvents(); + this.showInitialWindow(argv.includes('--open-url') ? argv[argv.length - 1] : undefined); + const port = await this.startBackend(); + this._backendPort.resolve(port); + await app.whenReady(); + await this.attachElectronSecurityToken(port); + await this.startContributions(); + + this.handleMainCommand({ + file: args.file, + cwd: process.cwd(), + secondInstance: false + }); + }, + ).parse(); } protected getTitleBarStyle(config: FrontendApplicationConfig): 'native' | 'custom' { @@ -276,25 +293,116 @@ export class ElectronMainApplication { return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom'; } - protected showInitialWindow(): void { - if (this.config.electron.showWindowEarly && - !('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1')) { - console.log('Showing main window early'); + protected async determineSplashScreenBounds(initialWindowBounds: { x: number, y: number, width: number, height: number }): + Promise<{ x: number, y: number, width: number, height: number }> { + const splashScreenOptions = this.getSplashScreenOptions(); + const width = splashScreenOptions?.width ?? 640; + const height = splashScreenOptions?.height ?? 480; + + // determine the screen on which to show the splash screen via the center of the window to show + const windowCenterPoint = { x: initialWindowBounds.x + (initialWindowBounds.width / 2), y: initialWindowBounds.y + (initialWindowBounds.height / 2) }; + const { bounds } = screen.getDisplayNearestPoint(windowCenterPoint); + + // place splash screen center of screen + const screenCenterPoint = { x: bounds.x + (bounds.width / 2), y: bounds.y + (bounds.height / 2) }; + const x = screenCenterPoint.x - (width / 2); + const y = screenCenterPoint.y - (height / 2); + + return { + x, y, width, height + }; + } + + protected isShowWindowEarly(): boolean { + return !!this.config.electron.showWindowEarly && + !('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1'); + } + + protected showInitialWindow(urlToOpen: string | undefined): void { + if (this.isShowWindowEarly() || this.isShowSplashScreen()) { app.whenReady().then(async () => { const options = await this.getLastWindowOptions(); + // If we want to show a splash screen, don't auto open the main window + if (this.isShowSplashScreen()) { + options.preventAutomaticShow = true; + } this.initialWindow = await this.createWindow({ ...options }); - this.initialWindow.show(); + TheiaRendererAPI.onApplicationStateChanged(this.initialWindow.webContents, state => { + if (state === 'ready' && urlToOpen) { + this.openUrl(urlToOpen); + } + }); + if (this.isShowSplashScreen()) { + console.log('Showing splash screen'); + this.configureAndShowSplashScreen(this.initialWindow); + } + + // Show main window early if windows shall be shown early and splash screen is not configured + if (this.isShowWindowEarly() && !this.isShowSplashScreen()) { + console.log('Showing main window early'); + this.initialWindow.show(); + } }); } } - protected async launch(params: ElectronMainExecutionParams): Promise { - createYargs(params.argv, params.cwd) - .command('$0 [file]', false, - cmd => cmd - .positional('file', { type: 'string' }), - args => this.handleMainCommand(params, { file: args.file }), - ).parse(); + protected async configureAndShowSplashScreen(mainWindow: BrowserWindow): Promise { + const splashScreenOptions = this.getSplashScreenOptions()!; + console.debug('SplashScreen options', splashScreenOptions); + + const splashScreenBounds = await this.determineSplashScreenBounds(mainWindow.getBounds()); + const splashScreenWindow = new BrowserWindow({ + ...splashScreenBounds, + frame: false, + alwaysOnTop: true, + show: false, + transparent: true, + webPreferences: { + backgroundThrottling: false + } + }); + + if (this.isShowWindowEarly()) { + console.log('Showing splash screen early'); + splashScreenWindow.show(); + } else { + splashScreenWindow.on('ready-to-show', () => { + splashScreenWindow.show(); + }); + } + + splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, splashScreenOptions.content!).toString()); + + // close splash screen and show main window once frontend is ready or a timeout is hit + const cancelTokenSource = new CancellationTokenSource(); + const minTime = timeout(splashScreenOptions.minDuration ?? 0, cancelTokenSource.token); + const maxTime = timeout(splashScreenOptions.maxDuration ?? 30000, cancelTokenSource.token); + + const showWindowAndCloseSplashScreen = () => { + cancelTokenSource.cancel(); + if (!mainWindow.isVisible()) { + mainWindow.show(); + } + splashScreenWindow.close(); + }; + TheiaRendererAPI.onApplicationStateChanged(mainWindow.webContents, state => { + if (state === 'ready') { + minTime.then(() => showWindowAndCloseSplashScreen()); + } + }); + maxTime.then(() => showWindowAndCloseSplashScreen()); + return splashScreenWindow; + } + + protected isShowSplashScreen(): boolean { + return typeof this.config.electron.splashScreenOptions === 'object' && !!this.config.electron.splashScreenOptions.content; + } + + protected getSplashScreenOptions(): ElectronFrontendApplicationConfig.SplashScreenOptions | undefined { + if (this.isShowSplashScreen()) { + return this.config.electron.splashScreenOptions; + } + return undefined; } /** @@ -307,13 +415,27 @@ export class ElectronMainApplication { options = this.avoidOverlap(options); const electronWindow = this.windowFactory(options, this.config); const id = electronWindow.window.webContents.id; + this.activeWindowStack.push(id); this.windows.set(id, electronWindow); - electronWindow.onDidClose(() => this.windows.delete(id)); + electronWindow.onDidClose(() => { + const stackIndex = this.activeWindowStack.indexOf(id); + if (stackIndex >= 0) { + this.activeWindowStack.splice(stackIndex, 1); + } + this.windows.delete(id); + }); electronWindow.window.on('maximize', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'maximize')); electronWindow.window.on('unmaximize', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'unmaximize')); - electronWindow.window.on('focus', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus')); + electronWindow.window.on('focus', () => { + const stackIndex = this.activeWindowStack.indexOf(id); + if (stackIndex >= 0) { + this.activeWindowStack.splice(stackIndex, 1); + } + this.activeWindowStack.unshift(id); + TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus'); + }); this.attachSaveWindowState(electronWindow.window); - this.configureNativeSecondaryWindowCreation(electronWindow.window); + return electronWindow.window; } @@ -360,6 +482,7 @@ export class ElectronMainApplication { // Setting the following option to `true` causes some features to break, somehow. // Issue: https://github.com/eclipse-theia/theia/issues/8577 nodeIntegrationInWorker: false, + backgroundThrottling: false, preload: path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib', 'frontend', 'preload.js').toString() }, ...this.config.electron?.windowOptions || {}, @@ -390,31 +513,6 @@ export class ElectronMainApplication { return window; } - /** Configures native window creation, i.e. using window.open or links with target "_blank" in the frontend. */ - protected configureNativeSecondaryWindowCreation(electronWindow: BrowserWindow): void { - electronWindow.webContents.setWindowOpenHandler(() => { - const { minWidth, minHeight } = this.getDefaultOptions(); - const options: BrowserWindowConstructorOptions = { - ...this.getDefaultTheiaWindowBounds(), - // We always need the native window frame for now because the secondary window does not have Theia's title bar by default. - // In 'custom' title bar mode this would leave the window without any window controls (close, min, max) - // TODO set to this.useNativeWindowFrame when secondary windows support a custom title bar. - frame: true, - minWidth, - minHeight - }; - if (!this.useNativeWindowFrame) { - // If the main window does not have a native window frame, do not show an icon in the secondary window's native title bar. - // The data url is a 1x1 transparent png - options.icon = nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12P4DwQACfsD/WMmxY8AAAAASUVORK5CYII='); - } - return { - action: 'allow', - overrideBrowserWindowOptions: options, - }; - }); - } - /** * "Gently" close all windows, application will not stop if a `beforeunload` handler returns `false`. */ @@ -422,15 +520,15 @@ export class ElectronMainApplication { app.quit(); } - protected async handleMainCommand(params: ElectronMainExecutionParams, options: ElectronMainCommandOptions): Promise { - if (params.secondInstance === false) { + protected async handleMainCommand(options: ElectronMainCommandOptions): Promise { + if (options.secondInstance === false) { await this.openWindowWithWorkspace(''); // restore previous workspace. } else if (options.file === undefined) { await this.openDefaultWindow(); } else { let workspacePath: string | undefined; try { - workspacePath = await fs.realpath(path.resolve(params.cwd, options.file)); + workspacePath = await fs.realpath(path.resolve(options.cwd, options.file)); } catch { console.error(`Could not resolve the workspace path. "${options.file}" is not a valid 'file' option. Falling back to the default workspace location.`); } @@ -442,6 +540,15 @@ export class ElectronMainApplication { } } + async openUrl(url: string): Promise { + for (const id of this.activeWindowStack) { + const window = this.windows.get(id); + if (window && await window.openUrl(url)) { + break; + } + } + } + protected async createWindowUri(params: WindowSearchParams = {}): Promise { if (!('port' in params)) { params.port = (await this.backendPort).toString(); @@ -461,6 +568,10 @@ export class ElectronMainApplication { }; } + protected getDefaultTheiaSecondaryWindowBounds(): TheiaBrowserWindowOptions { + return {}; + } + protected getDefaultTheiaWindowBounds(): TheiaBrowserWindowOptions { // The `screen` API must be required when the application is ready. // See: https://electronjs.org/docs/api/screen#screen @@ -542,10 +653,6 @@ export class ElectronMainApplication { protected async startBackend(): Promise { // Check if we should run everything as one process. const noBackendFork = process.argv.indexOf('--no-cluster') !== -1; - // We cannot use the `process.cwd()` as the application project path (the location of the `package.json` in other words) - // in a bundled electron application because it depends on the way we start it. For instance, on OS X, these are a differences: - // https://github.com/eclipse-theia/theia/issues/3297#issuecomment-439172274 - process.env.THEIA_APP_PROJECT_PATH = this.globals.THEIA_APP_PROJECT_PATH; // Set the electron version for both the dev and the production mode. (https://github.com/eclipse-theia/theia/issues/3254) // Otherwise, the forked backend processes will not know that they're serving the electron frontend. process.env.THEIA_ELECTRON_VERSION = process.versions.electron; @@ -616,6 +723,17 @@ export class ElectronMainApplication { app.on('will-quit', this.onWillQuit.bind(this)); app.on('second-instance', this.onSecondInstance.bind(this)); app.on('window-all-closed', this.onWindowAllClosed.bind(this)); + app.on('web-contents-created', this.onWebContentsCreated.bind(this)); + + if (isWindows) { + const args = this.processArgv.isBundledElectronApp ? [] : [app.getAppPath()]; + args.push('--open-url'); + app.setAsDefaultProtocolClient(this.config.electron.uriScheme, process.execPath, args); + } else { + app.on('open-url', (evt, url) => { + this.openUrl(url); + }); + } } protected onWillQuit(event: ElectronEvent): void { @@ -623,16 +741,79 @@ export class ElectronMainApplication { } protected async onSecondInstance(event: ElectronEvent, argv: string[], cwd: string): Promise { - const electronWindows = BrowserWindow.getAllWindows(); - if (electronWindows.length > 0) { - const electronWindow = electronWindows[0]; - if (electronWindow.isMinimized()) { - electronWindow.restore(); - } - electronWindow.focus(); + if (argv.includes('--open-url')) { + this.openUrl(argv[argv.length - 1]); + } else { + createYargs(this.processArgv.getProcessArgvWithoutBin(argv), process.cwd()) + .help(false) + .command('$0 [file]', false, + cmd => cmd + .positional('file', { type: 'string' }), + async args => { + await this.handleMainCommand({ + file: args.file, + cwd: process.cwd(), + secondInstance: true + }); + }, + ).parse(); } } + protected onWebContentsCreated(event: ElectronEvent, webContents: WebContents): void { + // Block any in-page navigation except loading the secondary window contents + webContents.on('will-navigate', evt => { + if (new URI(evt.url).path.fsPath() !== new Path(this.globals.THEIA_SECONDARY_WINDOW_HTML_PATH).fsPath()) { + evt.preventDefault(); + } + }); + + webContents.setWindowOpenHandler(details => { + // if it's a secondary window, allow it to open + if (new URI(details.url).path.fsPath() === new Path(this.globals.THEIA_SECONDARY_WINDOW_HTML_PATH).fsPath()) { + const { minWidth, minHeight } = this.getDefaultOptions(); + const options: BrowserWindowConstructorOptions = { + ...this.getDefaultTheiaSecondaryWindowBounds(), + // We always need the native window frame for now because the secondary window does not have Theia's title bar by default. + // In 'custom' title bar mode this would leave the window without any window controls (close, min, max) + // TODO set to this.useNativeWindowFrame when secondary windows support a custom title bar. + frame: true, + minWidth, + minHeight + }; + if (!this.useNativeWindowFrame) { + // If the main window does not have a native window frame, do not show an icon in the secondary window's native title bar. + // The data url is a 1x1 transparent png + options.icon = nativeImage.createFromDataURL( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12P4DwQACfsD/WMmxY8AAAAASUVORK5CYII='); + } + return { + action: 'allow', + overrideBrowserWindowOptions: options, + }; + } else { + const uri: URI = new URI(details.url); + let okToOpen = uri.scheme === 'https' || uri.scheme === 'http'; + if (!okToOpen) { + const button = dialog.showMessageBoxSync(BrowserWindow.fromWebContents(webContents)!, { + message: `Open link\n\n${details.url}\n\nin the system handler?`, + type: 'question', + title: 'Open Link', + buttons: ['OK', 'Cancel'], + defaultId: 1, + cancelId: 1 + }); + okToOpen = button === 0; + } + if (okToOpen) { + shell.openExternal(details.url, {}); + } + + return { action: 'deny' }; + } + }); + } + protected onWindowAllClosed(event: ElectronEvent): void { if (!this.restarting) { this.requestStop(); @@ -645,9 +826,8 @@ export class ElectronMainApplication { if (wrapper) { const listener = wrapper.onDidClose(async () => { listener.dispose(); - await this.launch({ + await this.handleMainCommand({ secondInstance: false, - argv: this.processArgv.getProcessArgvWithoutBin(process.argv), cwd: process.cwd() }); this.restarting = false; diff --git a/packages/core/src/electron-main/electron-main-constants.ts b/packages/core/src/electron-main/electron-main-constants.ts index e235ee354a50a..b63c6ec4ff20e 100644 --- a/packages/core/src/electron-main/electron-main-constants.ts +++ b/packages/core/src/electron-main/electron-main-constants.ts @@ -16,7 +16,8 @@ export const ElectronMainApplicationGlobals = Symbol('ElectronMainApplicationGlobals'); export interface ElectronMainApplicationGlobals { - readonly THEIA_APP_PROJECT_PATH: string - readonly THEIA_BACKEND_MAIN_PATH: string - readonly THEIA_FRONTEND_HTML_PATH: string + readonly THEIA_APP_PROJECT_PATH: string; + readonly THEIA_BACKEND_MAIN_PATH: string; + readonly THEIA_FRONTEND_HTML_PATH: string; + readonly THEIA_SECONDARY_WINDOW_HTML_PATH: string } diff --git a/packages/core/src/electron-common/messaging/electron-connection-handler.ts b/packages/core/src/electron-main/messaging/electron-connection-handler.ts similarity index 100% rename from packages/core/src/electron-common/messaging/electron-connection-handler.ts rename to packages/core/src/electron-main/messaging/electron-connection-handler.ts diff --git a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts index 421d5d01497ad..9a4d5c4f68ccb 100644 --- a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts +++ b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts @@ -16,15 +16,15 @@ import { WebContents } from '@theia/electron/shared/electron'; import { inject, injectable, named, postConstruct } from 'inversify'; -import { ContributionProvider } from '../../common/contribution-provider'; -import { MessagingContribution } from '../../node/messaging/messaging-contribution'; -import { ElectronConnectionHandler } from '../../electron-common/messaging/electron-connection-handler'; -import { ElectronMainApplicationContribution } from '../electron-main-application'; -import { ElectronMessagingService } from './electron-messaging-service'; +import { ConnectionHandlers } from '../../node/messaging/default-messaging-service'; import { AbstractChannel, Channel, ChannelMultiplexer, MessageProvider } from '../../common/message-rpc/channel'; -import { ConnectionHandler, Emitter, WriteBuffer } from '../../common'; +import { ConnectionHandler, ContributionProvider, Emitter, WriteBuffer } from '../../common'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer'; import { TheiaRendererAPI } from '../electron-api-main'; +import { MessagingService } from '../../node'; +import { ElectronMessagingService } from './electron-messaging-service'; +import { ElectronConnectionHandler } from './electron-connection-handler'; +import { ElectronMainApplicationContribution } from '../electron-main-application'; /** * This component replicates the role filled by `MessagingContribution` but for Electron. @@ -36,37 +36,54 @@ import { TheiaRendererAPI } from '../electron-api-main'; @injectable() export class ElectronMessagingContribution implements ElectronMainApplicationContribution, ElectronMessagingService { - @inject(ContributionProvider) @named(ElectronMessagingService.Contribution) protected readonly messagingContributions: ContributionProvider; @inject(ContributionProvider) @named(ElectronConnectionHandler) protected readonly connectionHandlers: ContributionProvider; - protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers(); + protected readonly channelHandlers = new ConnectionHandlers(); + /** * Each electron window has a main channel and its own multiplexer to route multiple client messages the same IPC connection. */ - protected readonly windowChannelMultiplexer = new Map(); + protected readonly openChannels = new Map(); @postConstruct() protected init(): void { TheiaRendererAPI.onIpcData((sender, data) => this.handleIpcEvent(sender, data)); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ipcChannel(spec: string, callback: (params: any, channel: Channel) => void): void { + this.channelHandlers.push(spec, callback); + } + + onStart(): void { + for (const contribution of this.messagingContributions.getContributions()) { + contribution.configure(this); + } + for (const connectionHandler of this.connectionHandlers.getContributions()) { + this.channelHandlers.push(connectionHandler.path, (params, channel) => { + connectionHandler.onConnection(channel); + }); + } + } + protected handleIpcEvent(sender: WebContents, data: Uint8Array): void { // Get the multiplexer for a given window id try { - const windowChannelData = this.windowChannelMultiplexer.get(sender.id) ?? this.createWindowChannelData(sender); - windowChannelData!.channel.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(data)); + const windowChannel = this.openChannels.get(sender.id) ?? this.createWindowChannel(sender); + windowChannel.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(data)); } catch (error) { console.error('IPC: Failed to handle message', { error, data }); } } - // Creates a new multiplexer for a given sender/window - protected createWindowChannelData(sender: Electron.WebContents): { channel: ElectronWebContentChannel, multiplexer: ChannelMultiplexer } { - const mainChannel = this.createWindowMainChannel(sender); + // Creates a new channel for a given sender/window + protected createWindowChannel(sender: Electron.WebContents): ElectronWebContentChannel { + const mainChannel = new ElectronWebContentChannel(sender); + const multiplexer = new ChannelMultiplexer(mainChannel); multiplexer.onDidOpenChannel(openEvent => { const { channel, id } = openEvent; @@ -75,41 +92,26 @@ export class ElectronMessagingContribution implements ElectronMainApplicationCon channel.onClose(() => console.debug(`Closing channel on service path '${id}'.`)); } }); - - sender.once('did-navigate', () => this.disposeMultiplexer(sender.id, multiplexer, 'Window was refreshed')); // When refreshing the browser window. - sender.once('destroyed', () => this.disposeMultiplexer(sender.id, multiplexer, 'Window was closed')); // When closing the browser window. - const data = { channel: mainChannel, multiplexer }; - this.windowChannelMultiplexer.set(sender.id, data); - return data; - } - - /** - * Creates the main channel to a window. - * @param sender The window that the channel should be established to. - */ - protected createWindowMainChannel(sender: WebContents): ElectronWebContentChannel { - return new ElectronWebContentChannel(sender); + sender.once('did-navigate', () => this.deleteChannel(sender.id, 'Window was refreshed')); + sender.once('destroyed', () => this.deleteChannel(sender.id, 'Window was closed')); + this.openChannels.set(sender.id, mainChannel); + return mainChannel; } - protected disposeMultiplexer(windowId: number, multiplexer: ChannelMultiplexer, reason: string): void { - multiplexer.onUnderlyingChannelClose({ reason }); - this.windowChannelMultiplexer.delete(windowId); - } - - onStart(): void { - for (const contribution of this.messagingContributions.getContributions()) { - contribution.configure(this); - } - for (const connectionHandler of this.connectionHandlers.getContributions()) { - this.channelHandlers.push(connectionHandler.path, (params, channel) => { - connectionHandler.onConnection(channel); + protected deleteChannel(senderId: number, reason: string): void { + const channel = this.openChannels.get(senderId); + if (channel) { + this.openChannels.delete(senderId); + channel.onCloseEmitter.fire({ + reason: reason }); } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ipcChannel(spec: string, callback: (params: any, channel: Channel) => void): void { - this.channelHandlers.push(spec, callback); + protected readonly wsHandlers = new ConnectionHandlers(); + + registerConnectionHandler(spec: string, callback: (params: MessagingService.PathParams, channel: Channel) => void): void { + this.wsHandlers.push(spec, callback); } } diff --git a/packages/core/src/electron-main/messaging/electron-messaging-service.ts b/packages/core/src/electron-main/messaging/electron-messaging-service.ts index d2f5aab7c086f..ac4a0d2831ba1 100644 --- a/packages/core/src/electron-main/messaging/electron-messaging-service.ts +++ b/packages/core/src/electron-main/messaging/electron-messaging-service.ts @@ -23,6 +23,7 @@ export interface ElectronMessagingService { */ ipcChannel(path: string, callback: (params: ElectronMessagingService.PathParams, socket: Channel) => void): void; } + export namespace ElectronMessagingService { export interface PathParams { [name: string]: string diff --git a/packages/core/src/electron-main/theia-electron-window.ts b/packages/core/src/electron-main/theia-electron-window.ts index 5c473ffdab2f6..720865f8f59e1 100644 --- a/packages/core/src/electron-main/theia-electron-window.ts +++ b/packages/core/src/electron-main/theia-electron-window.ts @@ -22,7 +22,7 @@ import { ElectronMainApplicationGlobals } from './electron-main-constants'; import { DisposableCollection, Emitter, Event } from '../common'; import { createDisposableListener } from './event-utils'; import { URI } from '../common/uri'; -import { FileUri } from '../node/file-uri'; +import { FileUri } from '../common/file-uri'; import { TheiaRendererAPI } from './electron-api-main'; /** @@ -37,6 +37,12 @@ export interface TheiaBrowserWindowOptions extends BrowserWindowConstructorOptio * in which case we want to invalidate the stored options and use the default options instead. */ screenLayout?: string; + /** + * By default, the window will be shown as soon as the content is ready to render. + * This can be prevented by handing over preventAutomaticShow: `true`. + * Use this for fine-grained control over when to show the window, e.g. to coordinate with a splash screen. + */ + preventAutomaticShow?: boolean; } export const TheiaBrowserWindowOptions = Symbol('TheiaBrowserWindowOptions'); @@ -52,6 +58,7 @@ enum ClosingState { @injectable() export class TheiaElectronWindow { + @inject(TheiaBrowserWindowOptions) protected readonly options: TheiaBrowserWindowOptions; @inject(WindowApplicationConfig) protected readonly config: WindowApplicationConfig; @inject(ElectronMainApplicationGlobals) protected readonly globals: ElectronMainApplicationGlobals; @@ -76,7 +83,9 @@ export class TheiaElectronWindow { protected init(): void { this._window = new BrowserWindow(this.options); this._window.setMenuBarVisibility(false); - this.attachReadyToShow(); + if (!this.options.preventAutomaticShow) { + this.attachReadyToShow(); + } this.restoreMaximizedState(); this.attachCloseListeners(); this.trackApplicationState(); @@ -129,8 +138,9 @@ export class TheiaElectronWindow { }, this.toDispose); } - protected doCloseWindow(): void { + protected async doCloseWindow(): Promise { this.closeIsConfirmed = true; + await TheiaRendererAPI.sendAboutToClose(this._window.webContents); this._window.close(); } @@ -138,14 +148,18 @@ export class TheiaElectronWindow { return this.handleStopRequest(() => this.doCloseWindow(), reason); } - protected reload(): void { - this.handleStopRequest(() => { + protected reload(newUrl?: string): void { + this.handleStopRequest(async () => { this.applicationState = 'init'; - this._window.reload(); + if (newUrl) { + this._window.loadURL(newUrl); + } else { + this._window.reload(); + } }, StopReason.Reload); } - protected async handleStopRequest(onSafeCallback: () => unknown, reason: StopReason): Promise { + protected async handleStopRequest(onSafeCallback: () => Promise, reason: StopReason): Promise { // Only confirm close to windows that have loaded our frontend. // Both the windows's URL and the FS path of the `index.html` should be converted to the "same" format to be able to compare them. (#11226) // Notes: @@ -186,7 +200,11 @@ export class TheiaElectronWindow { } protected attachReloadListener(): void { - this.toDispose.push(TheiaRendererAPI.onRequestReload(this.window.webContents, () => this.reload())); + this.toDispose.push(TheiaRendererAPI.onRequestReload(this.window.webContents, (newUrl?: string) => this.reload(newUrl))); + } + + openUrl(url: string): Promise { + return TheiaRendererAPI.openUrl(this.window.webContents, url); } dispose(): void { diff --git a/packages/core/src/electron-node/cli/electron-backend-cli-module.ts b/packages/core/src/electron-node/cli/electron-backend-cli-module.ts new file mode 100644 index 0000000000000..2b7a98176cb9f --- /dev/null +++ b/packages/core/src/electron-node/cli/electron-backend-cli-module.ts @@ -0,0 +1,24 @@ +// ***************************************************************************** +// 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 { ContainerModule } from 'inversify'; +import { ElectronCliContribution } from './electron-cli-contribution'; +import { CliContribution } from '../../node'; + +export default new ContainerModule(bind => { + bind(ElectronCliContribution).toSelf().inSingletonScope(); + bind(CliContribution).toService(ElectronCliContribution); +}); diff --git a/packages/core/src/electron-node/cli/electron-cli-contribution.ts b/packages/core/src/electron-node/cli/electron-cli-contribution.ts new file mode 100644 index 0000000000000..c8b5be382679a --- /dev/null +++ b/packages/core/src/electron-node/cli/electron-cli-contribution.ts @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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 { injectable } from 'inversify'; +import { Argv, Arguments } from 'yargs'; +import { CliContribution } from '../../node'; +import { MaybePromise } from '../../common'; + +@injectable() +export class ElectronCliContribution implements CliContribution { + + configure(conf: Argv): void { + conf.option('electronUserData', { + type: 'string', + describe: 'The area where the electron main process puts its data' + }); + } + + setArguments(args: Arguments): MaybePromise { + } + +} diff --git a/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts b/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts index 43494b9ffd4c5..d9bceab8db793 100644 --- a/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts +++ b/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts @@ -16,7 +16,7 @@ import * as http from 'http'; import { inject, injectable } from 'inversify'; -import { BackendRemoteService } from '../../node/backend-remote-service'; +import { BackendRemoteService } from '../../node/remote/backend-remote-service'; import { WsRequestValidatorContribution } from '../../node/ws-request-validators'; @injectable() diff --git a/packages/core/src/node/application-server.ts b/packages/core/src/node/application-server.ts index 011f90db35e60..80f6bd7055596 100644 --- a/packages/core/src/node/application-server.ts +++ b/packages/core/src/node/application-server.ts @@ -26,9 +26,9 @@ export class ApplicationServerImpl implements ApplicationServer { protected readonly applicationPackage: ApplicationPackage; getExtensionsInfos(): Promise { - const extensions = this.applicationPackage.extensionPackages; - const infos: ExtensionInfo[] = extensions.map(extension => ({ name: extension.name, version: extension.version })); - return Promise.resolve(infos); + // @ts-expect-error + const appInfo: ExtensionInfo[] = globalThis.extensionInfo; + return Promise.resolve(appInfo); } getApplicationInfo(): Promise { @@ -37,11 +37,22 @@ export class ApplicationServerImpl implements ApplicationServer { const name = pck.name; const version = pck.version; - return Promise.resolve({ name, version }); + return Promise.resolve({ + name, + version + }); } return Promise.resolve(undefined); } + getApplicationRoot(): Promise { + return Promise.resolve(this.applicationPackage.projectPath); + } + + getApplicationPlatform(): Promise { + return Promise.resolve(`${process.platform}-${process.arch}`); + } + async getBackendOS(): Promise { return OS.type(); } diff --git a/packages/core/src/node/backend-application-module.ts b/packages/core/src/node/backend-application-module.ts index e3461c473b956..f5a17db9bd514 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -21,7 +21,7 @@ import { bindContributionProvider, MessageService, MessageClient, ConnectionHandler, RpcConnectionHandler, CommandService, commandServicePath, messageServicePath, OSBackendProvider, OSBackendProviderPath } from '../common'; -import { BackendApplication, BackendApplicationContribution, BackendApplicationCliContribution, BackendApplicationServer } from './backend-application'; +import { BackendApplication, BackendApplicationContribution, BackendApplicationCliContribution, BackendApplicationServer, BackendApplicationPath } from './backend-application'; import { CliManager, CliContribution } from './cli'; import { IPCConnectionProvider } from './messaging'; import { ApplicationServerImpl } from './application-server'; @@ -41,7 +41,8 @@ import { bindNodeStopwatch, bindBackendStopwatchServer } from './performance'; import { OSBackendProviderImpl } from './os-backend-provider'; import { BackendRequestFacade } from './request/backend-request-facade'; import { FileSystemLocking, FileSystemLockingImpl } from './filesystem-locking'; -import { BackendRemoteService } from './backend-remote-service'; +import { BackendRemoteService } from './remote/backend-remote-service'; +import { RemoteCliContribution } from './remote/remote-cli-contribution'; decorate(injectable(), ApplicationPackage); @@ -101,10 +102,7 @@ export const backendApplicationModule = new ContainerModule(bind => { }) ).inSingletonScope(); - bind(ApplicationPackage).toDynamicValue(({ container }) => { - const { projectPath } = container.get(BackendApplicationCliContribution); - return new ApplicationPackage({ projectPath }); - }).inSingletonScope(); + bind(ApplicationPackage).toConstantValue(new ApplicationPackage({ projectPath: BackendApplicationPath })); bind(WsRequestValidator).toSelf().inSingletonScope(); bindContributionProvider(bind, WsRequestValidatorContribution); @@ -127,6 +125,7 @@ export const backendApplicationModule = new ContainerModule(bind => { bind(ProxyCliContribution).toSelf().inSingletonScope(); bind(CliContribution).toService(ProxyCliContribution); + bindContributionProvider(bind, RemoteCliContribution); bind(BackendRemoteService).toSelf().inSingletonScope(); bind(BackendRequestFacade).toSelf().inSingletonScope(); bind(ConnectionHandler).toDynamicValue( diff --git a/packages/core/src/node/backend-application.ts b/packages/core/src/node/backend-application.ts index a93376955750b..31549abbc5f79 100644 --- a/packages/core/src/node/backend-application.ts +++ b/packages/core/src/node/backend-application.ts @@ -27,9 +27,14 @@ import { CliContribution } from './cli'; import { Deferred } from '../common/promise-util'; import { environment } from '../common/index'; import { AddressInfo } from 'net'; -import { ApplicationPackage } from '@theia/application-package'; import { ProcessUtils } from './process-utils'; +/** + * The path to the application project directory. This is the directory where the application code is located. + * Mostly contains the `package.json` file and the `lib` directory. + */ +export const BackendApplicationPath = process.env.THEIA_APP_PROJECT_PATH || process.cwd(); + export type DnsResultOrder = 'ipv4first' | 'verbatim' | 'nodeDefault'; const APP_PROJECT_PATH = 'app-project-path'; @@ -115,7 +120,8 @@ export class BackendApplicationCliContribution implements CliContribution { ssl: boolean | undefined; cert: string | undefined; certkey: string | undefined; - projectPath: string; + /** @deprecated Use the `BackendApplicationPath` constant or `process.env.THEIA_APP_PROJECT_PATH` environment variable instead */ + projectPath = BackendApplicationPath; configure(conf: yargs.Argv): void { conf.option('port', { alias: 'p', description: 'The port the backend server listens on.', type: 'number', default: DEFAULT_PORT }); @@ -123,7 +129,7 @@ export class BackendApplicationCliContribution implements CliContribution { conf.option('ssl', { description: 'Use SSL (HTTPS), cert and certkey must also be set', type: 'boolean', default: DEFAULT_SSL }); conf.option('cert', { description: 'Path to SSL certificate.', type: 'string' }); conf.option('certkey', { description: 'Path to SSL certificate key.', type: 'string' }); - conf.option(APP_PROJECT_PATH, { description: 'Sets the application project directory', default: this.appProjectPath() }); + conf.option(APP_PROJECT_PATH, { description: 'Sets the application project directory', deprecated: true }); conf.option('dnsDefaultResultOrder', { type: 'string', description: 'Configure Node\'s DNS resolver default behavior, see https://nodejs.org/docs/latest-v18.x/api/dns.html#dnssetdefaultresultorderorder', @@ -138,19 +144,8 @@ export class BackendApplicationCliContribution implements CliContribution { this.ssl = args.ssl as boolean; this.cert = args.cert as string; this.certkey = args.certkey as string; - this.projectPath = args[APP_PROJECT_PATH] as string; this.dnsDefaultResultOrder = args.dnsDefaultResultOrder as DnsResultOrder; } - - protected appProjectPath(): string { - if (environment.electron.is()) { - if (process.env.THEIA_APP_PROJECT_PATH) { - return process.env.THEIA_APP_PROJECT_PATH; - } - throw new Error('The \'THEIA_APP_PROJECT_PATH\' environment variable must be set when running in electron.'); - } - return process.cwd(); - } } /** @@ -161,9 +156,6 @@ export class BackendApplication { protected readonly app: express.Application = express(); - @inject(ApplicationPackage) - protected readonly applicationPackage: ApplicationPackage; - @inject(ProcessUtils) protected readonly processUtils: ProcessUtils; @@ -352,7 +344,7 @@ export class BackendApplication { const acceptedEncodings = req.acceptsEncodings(); const gzUrl = `${req.url}.gz`; - const gzPath = path.join(this.applicationPackage.projectPath, 'lib', 'frontend', gzUrl); + const gzPath = path.join(BackendApplicationPath, 'lib', 'frontend', gzUrl); if (acceptedEncodings.indexOf('gzip') === -1 || !(await fs.pathExists(gzPath))) { next(); return; diff --git a/packages/core/src/node/cli.ts b/packages/core/src/node/cli.ts index 949d233dfaf2f..28b9e7f0c8aa7 100644 --- a/packages/core/src/node/cli.ts +++ b/packages/core/src/node/cli.ts @@ -38,7 +38,7 @@ export class CliManager { async initializeCli(argv: string[], postSetArguments: () => Promise, defaultCommand: () => Promise): Promise { const pack = require('../../package.json'); const version = pack.version; - const command = yargs.version(version); + const command = yargs(argv, process.cwd()).version(version); command.exitProcess(this.isExit()); for (const contrib of this.contributionsProvider.getContributions()) { contrib.configure(command); @@ -54,7 +54,7 @@ export class CliManager { await postSetArguments(); }) .command('$0', false, () => { }, defaultCommand) - .parse(argv); + .parse(); } protected isExit(): boolean { diff --git a/packages/core/src/node/env-variables/env-variables-server.ts b/packages/core/src/node/env-variables/env-variables-server.ts index 3d795a9063dde..eae08768da14f 100644 --- a/packages/core/src/node/env-variables/env-variables-server.ts +++ b/packages/core/src/node/env-variables/env-variables-server.ts @@ -21,7 +21,8 @@ import * as drivelist from 'drivelist'; import { pathExists, mkdir } from 'fs-extra'; import { EnvVariable, EnvVariablesServer } from '../../common/env-variables'; import { isWindows } from '../../common/os'; -import { FileUri } from '../file-uri'; +import { FileUri } from '../../common/file-uri'; +import { BackendApplicationPath } from '../backend-application'; @injectable() export class EnvVariablesServerImpl implements EnvVariablesServer { @@ -45,10 +46,12 @@ export class EnvVariablesServerImpl implements EnvVariablesServer { } protected async createConfigDirUri(): Promise { - let dataFolderPath: string = ''; - if (process.env.THEIA_APP_PROJECT_PATH) { - dataFolderPath = join(process.env.THEIA_APP_PROJECT_PATH, 'data'); + if (process.env.THEIA_CONFIG_DIR) { + // this has been explicitly set by the user, so we do not override its value + return FileUri.create(process.env.THEIA_CONFIG_DIR).toString(); } + + const dataFolderPath = join(BackendApplicationPath, 'data'); const userDataPath = join(dataFolderPath, 'user-data'); const dataFolderExists = this.pathExistenceCache[dataFolderPath] ??= await pathExists(dataFolderPath); if (dataFolderExists) { diff --git a/packages/core/src/node/file-uri.spec.ts b/packages/core/src/node/file-uri.spec.ts index 378fe71edfc99..f4689d6a58672 100644 --- a/packages/core/src/node/file-uri.spec.ts +++ b/packages/core/src/node/file-uri.spec.ts @@ -17,7 +17,7 @@ import * as os from 'os'; import * as path from 'path'; import * as chai from 'chai'; -import { FileUri } from './file-uri'; +import { FileUri } from '../common/file-uri'; import { isWindows } from '../common/os'; const expect = chai.expect; diff --git a/packages/core/src/node/i18n/theia-localization-contribution.ts b/packages/core/src/node/i18n/theia-localization-contribution.ts index 4f6ad11db6a9c..e02eacac442de 100644 --- a/packages/core/src/node/i18n/theia-localization-contribution.ts +++ b/packages/core/src/node/i18n/theia-localization-contribution.ts @@ -20,17 +20,21 @@ import { LocalizationContribution, LocalizationRegistry } from './localization-c @injectable() export class TheiaLocalizationContribution implements LocalizationContribution { async registerLocalizations(registry: LocalizationRegistry): Promise { - registry.registerLocalizationFromRequire('cs', require('../../../i18n/nls.cs.json')); - registry.registerLocalizationFromRequire('de', require('../../../i18n/nls.de.json')); - registry.registerLocalizationFromRequire('es', require('../../../i18n/nls.es.json')); + // Attempt to use the same languages as VS Code + // See https://code.visualstudio.com/docs/getstarted/locales#_available-locales + registry.registerLocalizationFromRequire('zh-cn', require('../../../i18n/nls.zh-cn.json')); + registry.registerLocalizationFromRequire('zh-tw', require('../../../i18n/nls.zh-tw.json')); registry.registerLocalizationFromRequire('fr', require('../../../i18n/nls.fr.json')); - registry.registerLocalizationFromRequire('hu', require('../../../i18n/nls.hu.json')); + registry.registerLocalizationFromRequire('de', require('../../../i18n/nls.de.json')); registry.registerLocalizationFromRequire('it', require('../../../i18n/nls.it.json')); + registry.registerLocalizationFromRequire('es', require('../../../i18n/nls.es.json')); registry.registerLocalizationFromRequire('ja', require('../../../i18n/nls.ja.json')); - registry.registerLocalizationFromRequire('pl', require('../../../i18n/nls.pl.json')); - registry.registerLocalizationFromRequire('pt-br', require('../../../i18n/nls.pt-br.json')); - registry.registerLocalizationFromRequire('pt-pt', require('../../../i18n/nls.pt-pt.json')); + registry.registerLocalizationFromRequire('ko', require('../../../i18n/nls.ko.json')); registry.registerLocalizationFromRequire('ru', require('../../../i18n/nls.ru.json')); - registry.registerLocalizationFromRequire('zh-cn', require('../../../i18n/nls.zh-cn.json')); + registry.registerLocalizationFromRequire('pt-br', require('../../../i18n/nls.pt-br.json')); + registry.registerLocalizationFromRequire('tr', require('../../../i18n/nls.tr.json')); + registry.registerLocalizationFromRequire('pl', require('../../../i18n/nls.pl.json')); + registry.registerLocalizationFromRequire('cs', require('../../../i18n/nls.cs.json')); + registry.registerLocalizationFromRequire('hu', require('../../../i18n/nls.hu.json')); } } diff --git a/packages/core/src/node/index.ts b/packages/core/src/node/index.ts index 192f8089d99de..c7d735f4f03d6 100644 --- a/packages/core/src/node/index.ts +++ b/packages/core/src/node/index.ts @@ -16,7 +16,7 @@ export * from './backend-application'; export * from './debug'; -export * from './file-uri'; +export * from '../common/file-uri'; export * from './messaging'; export * from './cli'; export { FileSystemLocking } from './filesystem-locking'; diff --git a/packages/core/src/node/logger-cli-contribution.ts b/packages/core/src/node/logger-cli-contribution.ts index 956d0d4d83946..0df9fc73afe0e 100644 --- a/packages/core/src/node/logger-cli-contribution.ts +++ b/packages/core/src/node/logger-cli-contribution.ts @@ -19,7 +19,7 @@ import { injectable } from 'inversify'; import { LogLevel } from '../common/logger'; import { CliContribution } from './cli'; import * as fs from 'fs-extra'; -import * as nsfw from 'nsfw'; +import { subscribe } from '@parcel/watcher'; import { Event, Emitter } from '../common/event'; import * as path from 'path'; @@ -89,13 +89,17 @@ export class LogLevelCliContribution implements CliContribution { } } - protected watchLogConfigFile(filename: string): Promise { - return nsfw(filename, async (events: nsfw.FileChangeEvent[]) => { + protected async watchLogConfigFile(filename: string): Promise { + await subscribe(filename, async (err, events) => { + if (err) { + console.log(`Error during log file watching ${filename}: ${err}`); + return; + } try { for (const event of events) { - switch (event.action) { - case nsfw.actions.CREATED: - case nsfw.actions.MODIFIED: + switch (event.type) { + case 'create': + case 'update': await this.slurpLogConfigFile(filename); this.logConfigChangedEvent.fire(undefined); break; @@ -104,8 +108,6 @@ export class LogLevelCliContribution implements CliContribution { } catch (e) { console.error(`Error reading log config file ${filename}: ${e}`); } - }).then((watcher: nsfw.NSFW) => { - watcher.start(); }); } diff --git a/packages/core/src/node/messaging/default-messaging-service.ts b/packages/core/src/node/messaging/default-messaging-service.ts new file mode 100644 index 0000000000000..37836a846e650 --- /dev/null +++ b/packages/core/src/node/messaging/default-messaging-service.ts @@ -0,0 +1,129 @@ +// ***************************************************************************** +// Copyright (C) 2018 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 { injectable, inject, named, interfaces, Container } from 'inversify'; +import { ContributionProvider, ConnectionHandler, bindContributionProvider, servicesPath } from '../../common'; +import { MessagingService } from './messaging-service'; +import { ConnectionContainerModule } from './connection-container-module'; +import Route = require('route-parser'); +import { Channel, ChannelMultiplexer } from '../../common/message-rpc/channel'; +import { FrontendConnectionService } from './frontend-connection-service'; +import { BackendApplicationContribution } from '../backend-application'; + +export const MessagingContainer = Symbol('MessagingContainer'); +export const MainChannel = Symbol('MainChannel'); + +@injectable() +export class DefaultMessagingService implements MessagingService, BackendApplicationContribution { + @inject(MessagingContainer) + protected readonly container: interfaces.Container; + + @inject(FrontendConnectionService) + protected readonly frontendConnectionService: FrontendConnectionService; + + @inject(ContributionProvider) @named(ConnectionContainerModule) + protected readonly connectionModules: ContributionProvider; + + @inject(ContributionProvider) @named(MessagingService.Contribution) + protected readonly contributions: ContributionProvider; + + protected readonly channelHandlers = new ConnectionHandlers(); + + initialize(): void { + this.registerConnectionHandler(servicesPath, (_, socket) => this.handleConnection(socket)); + for (const contribution of this.contributions.getContributions()) { + contribution.configure(this); + } + } + + registerConnectionHandler(path: string, callback: (params: MessagingService.PathParams, mainChannel: Channel) => void): void { + this.frontendConnectionService.registerConnectionHandler(path, callback); + } + + registerChannelHandler(spec: string, callback: (params: MessagingService.PathParams, channel: Channel) => void): void { + this.channelHandlers.push(spec, (params, channel) => callback(params, channel)); + } + + protected handleConnection(channel: Channel): void { + const multiplexer = new ChannelMultiplexer(channel); + const channelHandlers = this.getConnectionChannelHandlers(channel); + multiplexer.onDidOpenChannel(event => { + if (channelHandlers.route(event.id, event.channel)) { + console.debug(`Opening channel for service path '${event.id}'.`); + event.channel.onClose(() => console.info(`Closing channel on service path '${event.id}'.`)); + } + }); + } + + protected createMainChannelContainer(socket: Channel): Container { + const connectionContainer: Container = this.container.createChild() as Container; + connectionContainer.bind(MainChannel).toConstantValue(socket); + return connectionContainer; + } + + protected getConnectionChannelHandlers(socket: Channel): ConnectionHandlers { + const connectionContainer = this.createMainChannelContainer(socket); + bindContributionProvider(connectionContainer, ConnectionHandler); + connectionContainer.load(...this.connectionModules.getContributions()); + const connectionChannelHandlers = new ConnectionHandlers(this.channelHandlers); + const connectionHandlers = connectionContainer.getNamed>(ContributionProvider, ConnectionHandler); + for (const connectionHandler of connectionHandlers.getContributions(true)) { + connectionChannelHandlers.push(connectionHandler.path, (_, channel) => { + connectionHandler.onConnection(channel); + }); + } + return connectionChannelHandlers; + } + +} + +export class ConnectionHandlers { + protected readonly handlers: ((path: string, connection: T) => string | false)[] = []; + + constructor( + protected readonly parent?: ConnectionHandlers + ) { } + + push(spec: string, callback: (params: MessagingService.PathParams, connection: T) => void): void { + const route = new Route(spec); + const handler = (path: string, channel: T): string | false => { + const params = route.match(path); + if (!params) { + return false; + } + callback(params, channel); + return route.reverse(params); + }; + this.handlers.push(handler); + } + + route(path: string, connection: T): string | false { + for (const handler of this.handlers) { + try { + const result = handler(path, connection); + if (result) { + return result; + } + } catch (e) { + console.error(e); + } + } + if (this.parent) { + return this.parent.route(path, connection); + } + return false; + } +} diff --git a/packages/core/src/node/messaging/frontend-connection-service.ts b/packages/core/src/node/messaging/frontend-connection-service.ts new file mode 100644 index 0000000000000..5657e137b3512 --- /dev/null +++ b/packages/core/src/node/messaging/frontend-connection-service.ts @@ -0,0 +1,24 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { Channel } from '../../common/message-rpc/'; +import { MessagingService } from './messaging-service'; + +export const FrontendConnectionService = Symbol('FrontendConnectionService'); + +export interface FrontendConnectionService { + registerConnectionHandler(path: string, callback: (params: MessagingService.PathParams, mainChannel: Channel) => void): void; +} + diff --git a/packages/core/src/node/messaging/messaging-backend-module.ts b/packages/core/src/node/messaging/messaging-backend-module.ts index 4f549efb75922..baab3a350736e 100644 --- a/packages/core/src/node/messaging/messaging-backend-module.ts +++ b/packages/core/src/node/messaging/messaging-backend-module.ts @@ -15,23 +15,38 @@ // ***************************************************************************** import { ContainerModule } from 'inversify'; -import { bindContributionProvider } from '../../common'; -import { BackendApplicationContribution } from '../backend-application'; -import { MessagingContribution, MessagingContainer } from './messaging-contribution'; +import { ConnectionHandler, RpcConnectionHandler, bindContributionProvider } from '../../common'; +// import { BackendApplicationContribution } from '../backend-application'; +import { DefaultMessagingService, MessagingContainer } from './default-messaging-service'; import { ConnectionContainerModule } from './connection-container-module'; import { MessagingService } from './messaging-service'; import { MessagingListener, MessagingListenerContribution } from './messaging-listeners'; +import { FrontendConnectionService } from './frontend-connection-service'; +import { BackendApplicationContribution } from '../backend-application'; +import { connectionCloseServicePath } from '../../common/messaging/connection-management'; +import { WebsocketFrontendConnectionService } from './websocket-frontend-connection-service'; +import { WebsocketEndpoint } from './websocket-endpoint'; export const messagingBackendModule = new ContainerModule(bind => { bindContributionProvider(bind, ConnectionContainerModule); bindContributionProvider(bind, MessagingService.Contribution); - bind(MessagingService.Identifier).to(MessagingContribution).inSingletonScope(); - bind(MessagingContribution).toDynamicValue(({ container }) => { - const child = container.createChild(); - child.bind(MessagingContainer).toConstantValue(container); - return child.get(MessagingService.Identifier); - }).inSingletonScope(); - bind(BackendApplicationContribution).toService(MessagingContribution); + bind(DefaultMessagingService).toSelf().inSingletonScope(); + bind(MessagingService.Identifier).toService(DefaultMessagingService); + bind(BackendApplicationContribution).toService(DefaultMessagingService); + bind(MessagingContainer).toDynamicValue(({ container }) => container).inSingletonScope(); + bind(WebsocketEndpoint).toSelf().inSingletonScope(); + bind(BackendApplicationContribution).toService(WebsocketEndpoint); + bind(WebsocketFrontendConnectionService).toSelf().inSingletonScope(); + bind(FrontendConnectionService).toService(WebsocketFrontendConnectionService); bind(MessagingListener).toSelf().inSingletonScope(); bindContributionProvider(bind, MessagingListenerContribution); + + bind(ConnectionHandler).toDynamicValue(context => { + const connectionService = context.container.get(FrontendConnectionService); + return new RpcConnectionHandler(connectionCloseServicePath, () => ({ + markForClose: (channelId: string) => { + connectionService.markForClose(channelId); + } + })); + }).inSingletonScope(); }); diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts deleted file mode 100644 index 2e628a1795762..0000000000000 --- a/packages/core/src/node/messaging/messaging-contribution.ts +++ /dev/null @@ -1,197 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2018 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 * as http from 'http'; -import * as https from 'https'; -import { Server, Socket } from 'socket.io'; -import { injectable, inject, named, postConstruct, interfaces, Container } from 'inversify'; -import { ContributionProvider, ConnectionHandler, bindContributionProvider } from '../../common'; -import { IWebSocket, WebSocketChannel } from '../../common/messaging/web-socket-channel'; -import { BackendApplicationContribution } from '../backend-application'; -import { MessagingService } from './messaging-service'; -import { ConnectionContainerModule } from './connection-container-module'; -import Route = require('route-parser'); -import { WsRequestValidator } from '../ws-request-validators'; -import { MessagingListener } from './messaging-listeners'; -import { Channel, ChannelMultiplexer } from '../../common/message-rpc/channel'; - -export const MessagingContainer = Symbol('MessagingContainer'); - -@injectable() -export class MessagingContribution implements BackendApplicationContribution, MessagingService { - - @inject(MessagingContainer) - protected readonly container: interfaces.Container; - - @inject(ContributionProvider) @named(ConnectionContainerModule) - protected readonly connectionModules: ContributionProvider; - - @inject(ContributionProvider) @named(MessagingService.Contribution) - protected readonly contributions: ContributionProvider; - - @inject(WsRequestValidator) - protected readonly wsRequestValidator: WsRequestValidator; - - @inject(MessagingListener) - protected readonly messagingListener: MessagingListener; - - protected readonly wsHandlers = new MessagingContribution.ConnectionHandlers(); - protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers(); - - @postConstruct() - protected init(): void { - this.ws(WebSocketChannel.wsPath, (_, socket) => this.handleChannels(socket)); - for (const contribution of this.contributions.getContributions()) { - contribution.configure(this); - } - } - - wsChannel(spec: string, callback: (params: MessagingService.PathParams, channel: Channel) => void): void { - this.channelHandlers.push(spec, (params, channel) => callback(params, channel)); - } - - ws(spec: string, callback: (params: MessagingService.PathParams, socket: Socket) => void): void { - this.wsHandlers.push(spec, callback); - } - - protected checkAliveTimeout = 30000; // 30 seconds - protected maxHttpBufferSize = 1e8; // 100 MB - - onStart(server: http.Server | https.Server): void { - const socketServer = new Server(server, { - pingInterval: this.checkAliveTimeout, - pingTimeout: this.checkAliveTimeout * 2, - maxHttpBufferSize: this.maxHttpBufferSize - }); - // Accept every namespace by using /.*/ - socketServer.of(/.*/).on('connection', async socket => { - const request = socket.request; - // Socket.io strips the `origin` header of the incoming request - // We provide a `fix-origin` header in the `WebSocketConnectionProvider` - request.headers.origin = request.headers['fix-origin'] as string; - if (await this.allowConnect(socket.request)) { - await this.handleConnection(socket); - this.messagingListener.onDidWebSocketUpgrade(socket.request, socket); - } else { - socket.disconnect(true); - } - }); - } - - protected async handleConnection(socket: Socket): Promise { - const pathname = socket.nsp.name; - if (pathname && !this.wsHandlers.route(pathname, socket)) { - console.error('Cannot find a ws handler for the path: ' + pathname); - } - } - - protected async allowConnect(request: http.IncomingMessage): Promise { - try { - return this.wsRequestValidator.allowWsUpgrade(request); - } catch (e) { - return false; - } - } - - protected handleChannels(socket: Socket): void { - const socketChannel = new WebSocketChannel(this.toIWebSocket(socket)); - const multiplexer = new ChannelMultiplexer(socketChannel); - const channelHandlers = this.getConnectionChannelHandlers(socket); - multiplexer.onDidOpenChannel(event => { - if (channelHandlers.route(event.id, event.channel)) { - console.debug(`Opening channel for service path '${event.id}'.`); - event.channel.onClose(() => console.debug(`Closing channel on service path '${event.id}'.`)); - } - }); - } - - protected toIWebSocket(socket: Socket): IWebSocket { - return { - close: () => { - socket.removeAllListeners('disconnect'); - socket.removeAllListeners('error'); - socket.removeAllListeners('message'); - socket.disconnect(); - }, - isConnected: () => socket.connected, - onClose: cb => socket.on('disconnect', reason => cb(reason)), - onError: cb => socket.on('error', error => cb(error)), - onMessage: cb => socket.on('message', data => cb(data)), - send: message => socket.emit('message', message) - }; - } - - protected createSocketContainer(socket: Socket): Container { - const connectionContainer: Container = this.container.createChild() as Container; - connectionContainer.bind(Socket).toConstantValue(socket); - return connectionContainer; - } - - protected getConnectionChannelHandlers(socket: Socket): MessagingContribution.ConnectionHandlers { - const connectionContainer = this.createSocketContainer(socket); - bindContributionProvider(connectionContainer, ConnectionHandler); - connectionContainer.load(...this.connectionModules.getContributions()); - const connectionChannelHandlers = new MessagingContribution.ConnectionHandlers(this.channelHandlers); - const connectionHandlers = connectionContainer.getNamed>(ContributionProvider, ConnectionHandler); - for (const connectionHandler of connectionHandlers.getContributions(true)) { - connectionChannelHandlers.push(connectionHandler.path, (_, channel) => { - connectionHandler.onConnection(channel); - }); - } - return connectionChannelHandlers; - } - -} - -export namespace MessagingContribution { - export class ConnectionHandlers { - protected readonly handlers: ((path: string, connection: T) => string | false)[] = []; - - constructor( - protected readonly parent?: ConnectionHandlers - ) { } - - push(spec: string, callback: (params: MessagingService.PathParams, connection: T) => void): void { - const route = new Route(spec); - const handler = (path: string, channel: T): string | false => { - const params = route.match(path); - if (!params) { - return false; - } - callback(params, channel); - return route.reverse(params); - }; - this.handlers.push(handler); - } - - route(path: string, connection: T): string | false { - for (const handler of this.handlers) { - try { - const result = handler(path, connection); - if (result) { - return result; - } - } catch (e) { - console.error(e); - } - } - if (this.parent) { - return this.parent.route(path, connection); - } - return false; - } - } -} diff --git a/packages/core/src/node/messaging/messaging-service.ts b/packages/core/src/node/messaging/messaging-service.ts index 7d4ad45432e4e..38fa34643e2f0 100644 --- a/packages/core/src/node/messaging/messaging-service.ts +++ b/packages/core/src/node/messaging/messaging-service.ts @@ -14,7 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Socket } from 'socket.io'; import { Channel } from '../../common/message-rpc/channel'; export interface MessagingService { @@ -22,7 +21,7 @@ export interface MessagingService { * Accept a web socket channel on the given path. * A path supports the route syntax: https://github.com/rcs/route-parser#what-can-i-use-in-my-routes. */ - wsChannel(path: string, callback: (params: MessagingService.PathParams, channel: Channel) => void): void; + registerChannelHandler(path: string, handler: (params: MessagingService.PathParams, channel: Channel) => void): void; /** * Accept a web socket connection on the given path. * A path supports the route syntax: https://github.com/rcs/route-parser#what-can-i-use-in-my-routes. @@ -31,8 +30,9 @@ export interface MessagingService { * Prefer using web socket channels over establishing new web socket connection. Clients can handle only limited amount of web sockets * and excessive amount can cause performance degradation. All web socket channels share a single web socket connection. */ - ws(path: string, callback: (params: MessagingService.PathParams, socket: Socket) => void): void; + registerConnectionHandler(path: string, callback: (params: MessagingService.PathParams, mainChannel: Channel) => void): void; } + export namespace MessagingService { /** Inversify container identifier for the `MessagingService` component. */ export const Identifier = Symbol('MessagingService'); diff --git a/packages/core/src/node/messaging/test/test-web-socket-channel.ts b/packages/core/src/node/messaging/test/test-web-socket-channel.ts index 65c4ed1e641e9..199d5a3e4c7a1 100644 --- a/packages/core/src/node/messaging/test/test-web-socket-channel.ts +++ b/packages/core/src/node/messaging/test/test-web-socket-channel.ts @@ -1,3 +1,4 @@ +/* eslint-disable @theia/runtime-import-check */ // ***************************************************************************** // Copyright (C) 2018 TypeFox and others. // @@ -17,40 +18,44 @@ import * as http from 'http'; import * as https from 'https'; import { AddressInfo } from 'net'; -import { io, Socket } from 'socket.io-client'; -import { Channel, ChannelMultiplexer } from '../../../common/message-rpc/channel'; -import { IWebSocket, WebSocketChannel } from '../../../common/messaging/web-socket-channel'; +import { servicesPath } from '../../../common'; +import { WebSocketConnectionSource } from '../../../browser/messaging/ws-connection-source'; +import { Container, inject } from 'inversify'; +import { RemoteConnectionProvider, ServiceConnectionProvider } from '../../../browser/messaging/service-connection-provider'; +import { messagingFrontendModule } from '../../../browser/messaging/messaging-frontend-module'; +import { Socket, io } from 'socket.io-client'; + +const websocketUrl = Symbol('testWebsocketUrl'); +class TestWebsocketConnectionSource extends WebSocketConnectionSource { + @inject(websocketUrl) + readonly websocketUrl: string; + + protected override createWebSocketUrl(path: string): string { + return this.websocketUrl; + } + + protected override createWebSocket(url: string): Socket { + return io(url); + } +} export class TestWebSocketChannelSetup { - public readonly multiplexer: ChannelMultiplexer; - public readonly channel: Channel; + public readonly connectionProvider: ServiceConnectionProvider; constructor({ server, path }: { server: http.Server | https.Server, path: string }) { - const socket = io(`ws://localhost:${(server.address() as AddressInfo).port}${WebSocketChannel.wsPath}`); - this.channel = new WebSocketChannel(toIWebSocket(socket)); - this.multiplexer = new ChannelMultiplexer(this.channel); - socket.on('connect', () => { - this.multiplexer.open(path); - }); - socket.connect(); + const address = (server.address() as AddressInfo); + const url = `ws://${address.address}:${address.port}${servicesPath}`; + this.connectionProvider = this.createConnectionProvider(url); } -} -function toIWebSocket(socket: Socket): IWebSocket { - return { - close: () => { - socket.removeAllListeners('disconnect'); - socket.removeAllListeners('error'); - socket.removeAllListeners('message'); - socket.close(); - }, - isConnected: () => socket.connected, - onClose: cb => socket.on('disconnect', reason => cb(reason)), - onError: cb => socket.on('error', reason => cb(reason)), - onMessage: cb => socket.on('message', data => cb(data)), - send: message => socket.emit('message', message) - }; + protected createConnectionProvider(socketUrl: string): ServiceConnectionProvider { + const container = new Container(); + container.bind(websocketUrl).toConstantValue(socketUrl); + container.load(messagingFrontendModule); + container.rebind(WebSocketConnectionSource).to(TestWebsocketConnectionSource); + return container.get(RemoteConnectionProvider); + } } diff --git a/packages/core/src/node/messaging/websocket-endpoint.ts b/packages/core/src/node/messaging/websocket-endpoint.ts new file mode 100644 index 0000000000000..187603c77db6e --- /dev/null +++ b/packages/core/src/node/messaging/websocket-endpoint.ts @@ -0,0 +1,79 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { MessagingService } from './messaging-service'; +import * as http from 'http'; +import * as https from 'https'; +import { inject, injectable } from 'inversify'; +import { Server, Socket } from 'socket.io'; +import { WsRequestValidator } from '../ws-request-validators'; +import { MessagingListener } from './messaging-listeners'; +import { ConnectionHandlers } from './default-messaging-service'; +import { BackendApplicationContribution } from '../backend-application'; + +@injectable() +export class WebsocketEndpoint implements BackendApplicationContribution { + @inject(WsRequestValidator) + protected readonly wsRequestValidator: WsRequestValidator; + + @inject(MessagingListener) + protected readonly messagingListener: MessagingListener; + + protected checkAliveTimeout = 30000; // 30 seconds + protected maxHttpBufferSize = 1e8; // 100 MB + + protected readonly wsHandlers = new ConnectionHandlers(); + + registerConnectionHandler(spec: string, callback: (params: MessagingService.PathParams, socket: Socket) => void): void { + this.wsHandlers.push(spec, callback); + } + + onStart(server: http.Server | https.Server): void { + const socketServer = new Server(server, { + pingInterval: this.checkAliveTimeout, + pingTimeout: this.checkAliveTimeout * 2, + maxHttpBufferSize: this.maxHttpBufferSize + }); + // Accept every namespace by using /.*/ + socketServer.of(/.*/).on('connection', async socket => { + const request = socket.request; + // Socket.io strips the `origin` header of the incoming request + // We provide a `fix-origin` header in the `WebSocketConnectionProvider` + request.headers.origin = request.headers['fix-origin'] as string; + if (await this.allowConnect(socket.request)) { + await this.handleConnection(socket); + this.messagingListener.onDidWebSocketUpgrade(socket.request, socket); + } else { + socket.disconnect(true); + } + }); + } + + protected async allowConnect(request: http.IncomingMessage): Promise { + try { + return this.wsRequestValidator.allowWsUpgrade(request); + } catch (e) { + return false; + } + } + + protected async handleConnection(socket: Socket): Promise { + const pathname = socket.nsp.name; + if (pathname && !this.wsHandlers.route(pathname, socket)) { + console.error('Cannot find a ws handler for the path: ' + pathname); + } + } +} + diff --git a/packages/core/src/node/messaging/websocket-frontend-connection-service.ts b/packages/core/src/node/messaging/websocket-frontend-connection-service.ts new file mode 100644 index 0000000000000..22349d08c6689 --- /dev/null +++ b/packages/core/src/node/messaging/websocket-frontend-connection-service.ts @@ -0,0 +1,186 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { Channel, WriteBuffer } from '../../common/message-rpc'; +import { MessagingService } from './messaging-service'; +import { inject, injectable } from 'inversify'; +import { Socket } from 'socket.io'; +import { ConnectionHandlers } from './default-messaging-service'; +import { SocketWriteBuffer } from '../../common/messaging/socket-write-buffer'; +import { FrontendConnectionService } from './frontend-connection-service'; +import { AbstractChannel } from '../../common/message-rpc/channel'; +import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer'; +import { BackendApplicationConfigProvider } from '../backend-application-config-provider'; +import { WebsocketEndpoint } from './websocket-endpoint'; +import { ConnectionManagementMessages } from '../../common/messaging/connection-management'; +import { Disposable, DisposableCollection } from '../../common'; + +@injectable() +export class WebsocketFrontendConnectionService implements FrontendConnectionService { + + @inject(WebsocketEndpoint) + protected readonly websocketServer: WebsocketEndpoint; + + protected readonly wsHandlers = new ConnectionHandlers(); + protected readonly connectionsByFrontend = new Map(); + protected readonly closeTimeouts = new Map(); + protected readonly channelsMarkedForClose = new Set(); + + registerConnectionHandler(spec: string, callback: (params: MessagingService.PathParams, channel: Channel) => void): void { + this.websocketServer.registerConnectionHandler(spec, (params, socket) => this.handleConnection(socket, channel => callback(params, channel))); + } + + protected async handleConnection(socket: Socket, channelCreatedHandler: (channel: Channel) => void): Promise { + // eslint-disable-next-line prefer-const + let reconnectListener: (frontEndId: string) => void; + const initialConnectListener = (frontEndId: string) => { + socket.off(ConnectionManagementMessages.INITIAL_CONNECT, initialConnectListener); + socket.off(ConnectionManagementMessages.RECONNECT, reconnectListener); + if (this.connectionsByFrontend.has(frontEndId)) { + this.closeConnection(frontEndId, 'reconnecting same front end'); + } + const channel = this.createConnection(socket, frontEndId); + this.handleSocketDisconnect(socket, channel, frontEndId); + channelCreatedHandler(channel); + socket.emit(ConnectionManagementMessages.INITIAL_CONNECT); + }; + + reconnectListener = (frontEndId: string) => { + socket.off(ConnectionManagementMessages.INITIAL_CONNECT, initialConnectListener); + socket.off(ConnectionManagementMessages.RECONNECT, reconnectListener); + const channel = this.connectionsByFrontend.get(frontEndId); + if (channel) { + console.info(`Reconnecting to front end ${frontEndId}`); + socket.emit(ConnectionManagementMessages.RECONNECT, true); + channel.connect(socket); + this.handleSocketDisconnect(socket, channel, frontEndId); + const pendingTimeout = this.closeTimeouts.get(frontEndId); + clearTimeout(pendingTimeout); + this.closeTimeouts.delete(frontEndId); + } else { + console.info(`Reconnecting failed for ${frontEndId}`); + socket.emit(ConnectionManagementMessages.RECONNECT, false); + } + }; + socket.on(ConnectionManagementMessages.INITIAL_CONNECT, initialConnectListener); + socket.on(ConnectionManagementMessages.RECONNECT, reconnectListener); + } + + protected closeConnection(frontEndId: string, reason: string): void { + console.info(`closing connection for ${frontEndId}`); + const connection = this.connectionsByFrontend.get(frontEndId)!; // not called when no connection is present + + this.connectionsByFrontend.delete(frontEndId); + + const pendingTimeout = this.closeTimeouts.get(frontEndId); + clearTimeout(pendingTimeout); + this.closeTimeouts.delete(frontEndId); + + connection.onCloseEmitter.fire({ reason }); + connection.close(); + } + + protected createConnection(socket: Socket, frontEndId: string): ReconnectableSocketChannel { + console.info(`creating connection for ${frontEndId}`); + const channel = new ReconnectableSocketChannel(); + channel.connect(socket); + + this.connectionsByFrontend.set(frontEndId, channel); + return channel; + } + + handleSocketDisconnect(socket: Socket, channel: ReconnectableSocketChannel, frontEndId: string): void { + socket.on('disconnect', evt => { + console.info('socked closed'); + channel.disconnect(); + + const timeout = this.frontendConnectionTimeout(); + const isMarkedForClose = this.channelsMarkedForClose.delete(frontEndId); + if (timeout === 0 || isMarkedForClose) { + this.closeConnection(frontEndId, evt); + } else if (timeout > 0) { + console.info(`setting close timeout for id ${frontEndId} to ${timeout}`); + const handle = setTimeout(() => { + this.closeConnection(frontEndId, evt); + }, timeout); + this.closeTimeouts.set(frontEndId, handle); + } else { + // timeout < 0: never close the back end + } + }); + } + + markForClose(channelId: string): void { + this.channelsMarkedForClose.add(channelId); + } + + private frontendConnectionTimeout(): number { + const envValue = Number(process.env['FRONTEND_CONNECTION_TIMEOUT']); + if (!isNaN(envValue)) { + return envValue; + } + + return BackendApplicationConfigProvider.get().frontendConnectionTimeout; + } +} + +class ReconnectableSocketChannel extends AbstractChannel { + private socket: Socket | undefined; + private socketBuffer = new SocketWriteBuffer(); + private disposables = new DisposableCollection(); + + connect(socket: Socket): void { + this.disposables = new DisposableCollection(); + this.socket = socket; + const errorHandler = (err: Error) => { + this.onErrorEmitter.fire(err); + }; + this.disposables.push(Disposable.create(() => { + socket.off('error', errorHandler); + })); + socket.on('error', errorHandler); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dataListener = (data: any) => { + // In the browser context socketIO receives binary messages as ArrayBuffers. + // So we have to convert them to a Uint8Array before delegating the message to the read buffer. + const buffer = data instanceof ArrayBuffer ? new Uint8Array(data) : data; + this.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(buffer)); + }; + this.disposables.push(Disposable.create(() => { + socket.off('message', dataListener); + })); + socket.on('message', dataListener); + this.socketBuffer.flush(socket); + } + + disconnect(): void { + this.disposables.dispose(); + this.socket = undefined; + } + + override getWriteBuffer(): WriteBuffer { + const writeBuffer = new Uint8ArrayWriteBuffer(); + writeBuffer.onCommit(data => { + if (this.socket?.connected) { + this.socket.send(data); + } else { + this.socketBuffer.buffer(data); + } + }); + return writeBuffer; + } +} + diff --git a/packages/core/src/node/backend-remote-service.ts b/packages/core/src/node/remote/backend-remote-service.ts similarity index 100% rename from packages/core/src/node/backend-remote-service.ts rename to packages/core/src/node/remote/backend-remote-service.ts diff --git a/packages/core/src/node/remote/remote-cli-contribution.ts b/packages/core/src/node/remote/remote-cli-contribution.ts new file mode 100644 index 0000000000000..7f25cb3fb952c --- /dev/null +++ b/packages/core/src/node/remote/remote-cli-contribution.ts @@ -0,0 +1,34 @@ +// ***************************************************************************** +// 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 type { OS } from '../../common/os'; +import type { MaybePromise } from '../../common/types'; + +export interface RemotePlatform { + os: OS.Type + arch: string +} + +export interface RemoteCliContext { + platform: RemotePlatform; + directory: string; +} + +export const RemoteCliContribution = Symbol('RemoteCliContribution'); + +export interface RemoteCliContribution { + enhanceArgs(context: RemoteCliContext): MaybePromise; +} diff --git a/packages/core/src/node/remote/remote-copy-contribution.ts b/packages/core/src/node/remote/remote-copy-contribution.ts new file mode 100644 index 0000000000000..c0a746c71679d --- /dev/null +++ b/packages/core/src/node/remote/remote-copy-contribution.ts @@ -0,0 +1,45 @@ +// ***************************************************************************** +// 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 { MaybePromise } from '../../common/types'; + +export const RemoteCopyContribution = Symbol('RemoteCopyContribution'); + +export interface RemoteCopyContribution { + copy(registry: RemoteCopyRegistry): MaybePromise +} + +export interface RemoteCopyOptions { + /** + * The mode that the file should be set to once copied to the remote. + * + * Only relevant for POSIX-like systems + */ + mode?: number; +} + +export interface RemoteFile { + path: string + target: string + options?: RemoteCopyOptions; +} + +export interface RemoteCopyRegistry { + getFiles(): RemoteFile[]; + glob(pattern: string, target?: string): Promise; + file(file: string, target?: string, options?: RemoteCopyOptions): void; + directory(dir: string, target?: string): Promise; +} diff --git a/packages/debug/package.json b/packages/debug/package.json index 364fc079a0917..bb7dcb3e6d939 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -1,25 +1,27 @@ { "name": "@theia/debug", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Debug Extension", "dependencies": { - "@theia/console": "1.44.0", - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/markers": "1.44.0", - "@theia/monaco": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/output": "1.44.0", - "@theia/process": "1.44.0", - "@theia/task": "1.44.0", - "@theia/terminal": "1.44.0", - "@theia/variable-resolver": "1.44.0", - "@theia/workspace": "1.44.0", + "@theia/console": "1.54.0", + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/markers": "1.54.0", + "@theia/monaco": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", + "@theia/output": "1.54.0", + "@theia/process": "1.54.0", + "@theia/task": "1.54.0", + "@theia/test": "1.54.0", + "@theia/terminal": "1.54.0", + "@theia/variable-resolver": "1.54.0", + "@theia/workspace": "1.54.0", "@vscode/debugprotocol": "^1.51.0", "fast-deep-equal": "^3.1.3", "jsonc-parser": "^2.2.0", - "p-debounce": "^2.1.0" + "p-debounce": "^2.1.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -27,6 +29,7 @@ "theiaExtensions": [ { "frontend": "lib/browser/debug-frontend-module", + "secondaryWindow": "lib/browser/debug-frontend-module", "backend": "lib/node/debug-backend-module" } ], @@ -56,7 +59,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/debug/src/browser/debug-frontend-application-contribution.ts b/packages/debug/src/browser/debug-frontend-application-contribution.ts index 5e833a8a39f29..51401d39a53ec 100644 --- a/packages/debug/src/browser/debug-frontend-application-contribution.ts +++ b/packages/debug/src/browser/debug-frontend-application-contribution.ts @@ -42,7 +42,7 @@ import { DebugConsoleContribution } from './console/debug-console-contribution'; import { DebugService } from '../common/debug-service'; import { DebugSchemaUpdater } from './debug-schema-updater'; import { DebugPreferences } from './debug-preferences'; -import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { TabBarToolbarContribution, TabBarToolbarRegistry, RenderedToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { DebugWatchWidget } from './view/debug-watch-widget'; import { DebugWatchExpression } from './view/debug-watch-expression'; import { DebugWatchManager } from './debug-watch-manager'; @@ -1079,7 +1079,7 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi registerToolbarItems(toolbar: TabBarToolbarRegistry): void { const onDidChangeToggleBreakpointsEnabled = new Emitter(); - const toggleBreakpointsEnabled: Mutable = { + const toggleBreakpointsEnabled: Mutable = { id: DebugCommands.TOGGLE_BREAKPOINTS_ENABLED.id, command: DebugCommands.TOGGLE_BREAKPOINTS_ENABLED.id, icon: codicon('activate-breakpoints'), diff --git a/packages/debug/src/browser/debug-frontend-module.ts b/packages/debug/src/browser/debug-frontend-module.ts index d00eb179a8a9f..05a6e80031ed9 100644 --- a/packages/debug/src/browser/debug-frontend-module.ts +++ b/packages/debug/src/browser/debug-frontend-module.ts @@ -49,7 +49,6 @@ import { CommandContribution } from '@theia/core/lib/common/command'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { DebugWatchManager } from './debug-watch-manager'; -import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; import { DebugBreakpointWidget } from './editor/debug-breakpoint-widget'; import { DebugInlineValueDecorator } from './editor/debug-inline-value-decorator'; import { JsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store'; @@ -61,6 +60,8 @@ import { DebugViewModel } from './view/debug-view-model'; import { DebugToolBar } from './view/debug-toolbar-widget'; import { DebugSessionWidget } from './view/debug-session-widget'; import { bindDisassemblyView } from './disassembly-view/disassembly-view-contribution'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; export default new ContainerModule((bind: interfaces.Bind) => { bindContributionProvider(bind, DebugContribution); @@ -78,7 +79,7 @@ export default new ContainerModule((bind: interfaces.Bind) => { DebugEditorModel.createModel(container, editor) )).inSingletonScope(); bind(DebugEditorService).toSelf().inSingletonScope().onActivation((context, service) => { - context.container.get(MonacoEditorService).registerDecorationType('Debug breakpoint placeholder', DebugBreakpointWidget.PLACEHOLDER_DECORATION, {}); + StandaloneServices.get(ICodeEditorService).registerDecorationType('Debug breakpoint placeholder', DebugBreakpointWidget.PLACEHOLDER_DECORATION, {}); return service; }); diff --git a/packages/debug/src/browser/debug-session-contribution.ts b/packages/debug/src/browser/debug-session-contribution.ts index 04d36c3c645fb..7639afb6bfa1b 100644 --- a/packages/debug/src/browser/debug-session-contribution.ts +++ b/packages/debug/src/browser/debug-session-contribution.ts @@ -19,7 +19,6 @@ import { MessageClient } from '@theia/core/lib/common'; import { LabelProvider } from '@theia/core/lib/browser'; import { EditorManager } from '@theia/editor/lib/browser'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; -import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider'; import { DebugSession } from './debug-session'; import { BreakpointManager } from './breakpoint/breakpoint-manager'; import { DebugConfigurationSessionOptions, DebugSessionOptions } from './debug-session-options'; @@ -31,6 +30,9 @@ import { ContributionProvider } from '@theia/core/lib/common/contribution-provid import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { DebugContribution } from './debug-contribution'; import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider'; +import { TestService } from '@theia/test/lib/browser/test-service'; +import { DebugSessionManager } from './debug-session-manager'; /** * DebugSessionContribution symbol for DI. @@ -90,13 +92,13 @@ export const DebugSessionFactory = Symbol('DebugSessionFactory'); * The [debug session](#DebugSession) factory. */ export interface DebugSessionFactory { - get(sessionId: string, options: DebugSessionOptions, parentSession?: DebugSession): DebugSession; + get(manager: DebugSessionManager, sessionId: string, options: DebugSessionOptions, parentSession?: DebugSession): DebugSession; } @injectable() export class DefaultDebugSessionFactory implements DebugSessionFactory { - @inject(WebSocketConnectionProvider) - protected readonly connectionProvider: WebSocketConnectionProvider; + @inject(RemoteConnectionProvider) + protected readonly connectionProvider: ServiceConnectionProvider; @inject(TerminalService) protected readonly terminalService: TerminalService; @inject(EditorManager) @@ -115,22 +117,27 @@ export class DefaultDebugSessionFactory implements DebugSessionFactory { protected readonly fileService: FileService; @inject(ContributionProvider) @named(DebugContribution) protected readonly debugContributionProvider: ContributionProvider; + @inject(TestService) + protected readonly testService: TestService; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; - get(sessionId: string, options: DebugConfigurationSessionOptions, parentSession?: DebugSession): DebugSession { + get(manager: DebugSessionManager, sessionId: string, options: DebugConfigurationSessionOptions, parentSession?: DebugSession): DebugSession { const connection = new DebugSessionConnection( sessionId, () => new Promise(resolve => - this.connectionProvider.openChannel(`${DebugAdapterPath}/${sessionId}`, wsChannel => { + this.connectionProvider.listen(`${DebugAdapterPath}/${sessionId}`, (_, wsChannel) => { resolve(new ForwardingDebugChannel(wsChannel)); - }, { reconnecting: false }) + }, false) ), this.getTraceOutputChannel()); return new DebugSession( sessionId, options, parentSession, + this.testService, + options.testRun, + manager, connection, this.terminalService, this.editorManager, diff --git a/packages/debug/src/browser/debug-session-manager.ts b/packages/debug/src/browser/debug-session-manager.ts index adae46254f038..a01339d5cf01e 100644 --- a/packages/debug/src/browser/debug-session-manager.ts +++ b/packages/debug/src/browser/debug-session-manager.ts @@ -55,11 +55,6 @@ export interface DidChangeBreakpointsEvent { uri: URI } -export interface DidFocusStackFrameEvent { - session: DebugSession; - frame: DebugStackFrame | undefined; -} - export interface DebugSessionCustomEvent { readonly body?: any // eslint-disable-line @typescript-eslint/no-explicit-any readonly event: string @@ -94,9 +89,12 @@ export class DebugSessionManager { protected readonly onDidReceiveDebugSessionCustomEventEmitter = new Emitter(); readonly onDidReceiveDebugSessionCustomEvent: Event = this.onDidReceiveDebugSessionCustomEventEmitter.event; - protected readonly onDidFocusStackFrameEmitter = new Emitter(); + protected readonly onDidFocusStackFrameEmitter = new Emitter(); readonly onDidFocusStackFrame = this.onDidFocusStackFrameEmitter.event; + protected readonly onDidFocusThreadEmitter = new Emitter(); + readonly onDidFocusThread = this.onDidFocusThreadEmitter.event; + protected readonly onDidChangeBreakpointsEmitter = new Emitter(); readonly onDidChangeBreakpoints = this.onDidChangeBreakpointsEmitter.event; protected fireDidChangeBreakpoints(event: DidChangeBreakpointsEvent): void { @@ -179,7 +177,7 @@ export class DebugSessionManager { } isCurrentEditorFrame(uri: URI | string | monaco.Uri): boolean { - return this.currentFrame?.source?.uri.toString() === (uri instanceof URI ? uri : new URI(uri)).toString(); + return this.currentFrame?.source?.uri.toString() === (uri instanceof URI ? uri : new URI(uri.toString())).toString(); } protected async saveAll(): Promise { @@ -388,7 +386,7 @@ export class DebugSessionManager { const parentSession = options.configuration.parentSessionId ? this._sessions.get(options.configuration.parentSessionId) : undefined; const contrib = this.sessionContributionRegistry.get(options.configuration.type); const sessionFactory = contrib ? contrib.debugSessionFactory() : this.debugSessionFactory; - const session = sessionFactory.get(sessionId, options, parentSession); + const session = sessionFactory.get(this, sessionId, options, parentSession); this._sessions.set(sessionId, session); this.debugTypeKey.set(session.configuration.type); @@ -518,7 +516,10 @@ export class DebugSessionManager { } this.fireDidChange(current); })); - this.disposeOnCurrentSessionChanged.push(current.onDidFocusStackFrame(frame => this.onDidFocusStackFrameEmitter.fire({ session: current, frame }))); + this.disposeOnCurrentSessionChanged.push(current.onDidFocusStackFrame(frame => this.onDidFocusStackFrameEmitter.fire(frame))); + this.disposeOnCurrentSessionChanged.push(current.onDidFocusThread(thread => this.onDidFocusThreadEmitter.fire(thread))); + const { currentThread } = current; + this.onDidFocusThreadEmitter.fire(currentThread); } this.updateBreakpoints(previous, current); this.open(); @@ -526,7 +527,7 @@ export class DebugSessionManager { } open(): void { const { currentFrame } = this; - if (currentFrame) { + if (currentFrame && currentFrame.thread.stopped) { currentFrame.open(); } } diff --git a/packages/debug/src/browser/debug-session-options.ts b/packages/debug/src/browser/debug-session-options.ts index 3e61d0ec4dfaa..8a94ade9d8636 100644 --- a/packages/debug/src/browser/debug-session-options.ts +++ b/packages/debug/src/browser/debug-session-options.ts @@ -31,8 +31,14 @@ export class DebugCompoundRoot { } } +export interface TestRunReference { + controllerId: string, + runId: string +} + export interface DebugSessionOptionsBase { workspaceFolderUri?: string, + testRun?: TestRunReference } export interface DebugConfigurationSessionOptions extends DebugSessionOptionsBase { diff --git a/packages/debug/src/browser/debug-session.tsx b/packages/debug/src/browser/debug-session.tsx index 66b86ec487b7e..06f7f0140b9c2 100644 --- a/packages/debug/src/browser/debug-session.tsx +++ b/packages/debug/src/browser/debug-session.tsx @@ -33,7 +33,7 @@ import { DebugSourceBreakpoint } from './model/debug-source-breakpoint'; import debounce = require('p-debounce'); import URI from '@theia/core/lib/common/uri'; import { BreakpointManager } from './breakpoint/breakpoint-manager'; -import { DebugConfigurationSessionOptions, InternalDebugSessionOptions } from './debug-session-options'; +import { DebugConfigurationSessionOptions, InternalDebugSessionOptions, TestRunReference } from './debug-session-options'; import { DebugConfiguration, DebugConsoleMode } from '../common/debug-common'; import { SourceBreakpoint, ExceptionBreakpoint } from './breakpoint/breakpoint-marker'; import { TerminalWidgetOptions, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; @@ -44,6 +44,8 @@ import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { DebugInstructionBreakpoint } from './model/debug-instruction-breakpoint'; import { nls } from '@theia/core'; +import { TestService, TestServices } from '@theia/test/lib/browser/test-service'; +import { DebugSessionManager } from './debug-session-manager'; export enum DebugState { Inactive, @@ -78,6 +80,11 @@ export class DebugSession implements CompositeTreeElement { return this.onDidFocusStackFrameEmitter.event; } + protected readonly onDidFocusThreadEmitter = new Emitter(); + get onDidFocusThread(): Event { + return this.onDidFocusThreadEmitter.event; + } + protected readonly onDidChangeBreakpointsEmitter = new Emitter(); readonly onDidChangeBreakpoints: Event = this.onDidChangeBreakpointsEmitter.event; protected fireDidChangeBreakpoints(uri: URI): void { @@ -93,6 +100,9 @@ export class DebugSession implements CompositeTreeElement { readonly id: string, readonly options: DebugConfigurationSessionOptions, readonly parentSession: DebugSession | undefined, + testService: TestService, + testRun: TestRunReference | undefined, + sessionManager: DebugSessionManager, protected readonly connection: DebugSessionConnection, protected readonly terminalServer: TerminalService, protected readonly editorManager: EditorManager, @@ -119,6 +129,19 @@ export class DebugSession implements CompositeTreeElement { this.parentSession?.childSessions?.delete(id); })); } + if (testRun) { + try { + const run = TestServices.withTestRun(testService, testRun.controllerId, testRun.runId); + run.onDidChangeProperty(evt => { + if (evt.isRunning === false) { + sessionManager.terminateSession(this); + } + }); + } catch (err) { + console.error(err); + } + } + this.connection.onDidClose(() => this.toDispose.dispose()); this.toDispose.pushAll([ this.onDidChangeEmitter, @@ -254,8 +277,12 @@ export class DebugSession implements CompositeTreeElement { return this._currentThread; } set currentThread(thread: DebugThread | undefined) { + if (this._currentThread?.id === thread?.id) { + return; + } this.toDisposeOnCurrentThread.dispose(); this._currentThread = thread; + this.onDidFocusThreadEmitter.fire(thread); this.fireDidChange(); if (thread) { this.toDisposeOnCurrentThread.push(thread.onDidChanged(() => this.fireDidChange())); diff --git a/packages/debug/src/browser/disassembly-view/disassembly-view-instruction-renderer.ts b/packages/debug/src/browser/disassembly-view/disassembly-view-instruction-renderer.ts index 1fb92dfd9fd23..e43eea44d9a02 100644 --- a/packages/debug/src/browser/disassembly-view/disassembly-view-instruction-renderer.ts +++ b/packages/debug/src/browser/disassembly-view/disassembly-view-instruction-renderer.ts @@ -110,8 +110,8 @@ export class InstructionRenderer extends Disposable implements ITableRenderer= 1 && lineNumber <= textModel.getLineCount()) { const lineContent = textModel.getLineContent(lineNumber); - sourceSB.appendASCIIString(` ${lineNumber}: `); - sourceSB.appendASCIIString(lineContent + '\n'); + sourceSB.appendString(` ${lineNumber}: `); + sourceSB.appendString(lineContent + '\n'); if (instruction.endLine && lineNumber < instruction.endLine) { lineNumber++; @@ -129,27 +129,27 @@ export class InstructionRenderer extends Disposable implements ITableRenderer DebugEditorModel; @@ -94,9 +94,6 @@ export class DebugEditorModel implements Disposable { @inject(DebugInlineValueDecorator) readonly inlineValueDecorator: DebugInlineValueDecorator; - @inject(MonacoConfigurationService) - readonly configurationService: IConfigurationService; - @inject(DebugSessionManager) protected readonly sessionManager: DebugSessionManager; @@ -156,7 +153,7 @@ export class DebugEditorModel implements Disposable { resource: model.uri, overrideIdentifier: model.getLanguageId(), }; - const { enabled, delay, sticky } = this.configurationService.getValue('editor.hover', overrides); + const { enabled, delay, sticky } = StandaloneServices.get(IConfigurationService).getValue('editor.hover', overrides); codeEditor.updateOptions({ hover: { enabled, @@ -193,6 +190,10 @@ export class DebugEditorModel implements Disposable { return []; } + if (!currentFrame.thread.stopped) { + return []; + } + if (!this.sessions.isCurrentEditorFrame(this.uri)) { return []; } diff --git a/packages/debug/src/browser/editor/debug-inline-value-decorator.ts b/packages/debug/src/browser/editor/debug-inline-value-decorator.ts index 92c7a77ca58fa..00b27fd05eaa2 100644 --- a/packages/debug/src/browser/editor/debug-inline-value-decorator.ts +++ b/packages/debug/src/browser/editor/debug-inline-value-decorator.ts @@ -31,11 +31,11 @@ import { InlineValueContext } from '@theia/monaco-editor-core/esm/vs/editor/comm import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model'; 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 { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; import { DebugVariable, ExpressionContainer, ExpressionItem } from '../console/debug-console-items'; import { DebugPreferences } from '../debug-preferences'; import { DebugStackFrame } from '../model/debug-stack-frame'; import { DebugEditorModel } from './debug-editor-model'; +import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L40-L43 export const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration'; @@ -59,10 +59,6 @@ class InlineSegment { @injectable() export class DebugInlineValueDecorator implements FrontendApplicationContribution { - - @inject(MonacoEditorService) - protected readonly editorService: MonacoEditorService; - @inject(DebugPreferences) protected readonly preferences: DebugPreferences; @@ -70,7 +66,7 @@ export class DebugInlineValueDecorator implements FrontendApplicationContributio protected wordToLineNumbersMap: Map | undefined = new Map(); onStart(): void { - this.editorService.registerDecorationType('Inline debug decorations', INLINE_VALUE_DECORATION_KEY, {}); + StandaloneServices.get(ICodeEditorService).registerDecorationType('Inline debug decorations', INLINE_VALUE_DECORATION_KEY, {}); this.enabled = !!this.preferences['debug.inlineValues']; this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => { if (preferenceName === 'debug.inlineValues' && !!newValue !== this.enabled) { diff --git a/packages/debug/src/browser/model/debug-stack-frame.tsx b/packages/debug/src/browser/model/debug-stack-frame.tsx index d03831d67dae4..65ad04af48b27 100644 --- a/packages/debug/src/browser/model/debug-stack-frame.tsx +++ b/packages/debug/src/browser/model/debug-stack-frame.tsx @@ -49,6 +49,13 @@ export class DebugStackFrame extends DebugStackFrameData implements TreeElement return this.session.id + ':' + this.thread.id + ':' + this.raw.id; } + /** + * Returns the frame identifier from the debug protocol. + */ + get frameId(): number { + return this.raw.id; + } + protected _source: DebugSource | undefined; get source(): DebugSource | undefined { return this._source; diff --git a/packages/debug/src/browser/model/debug-thread.tsx b/packages/debug/src/browser/model/debug-thread.tsx index 1aae18af9d98a..306ff5d1082f6 100644 --- a/packages/debug/src/browser/model/debug-thread.tsx +++ b/packages/debug/src/browser/model/debug-thread.tsx @@ -56,11 +56,18 @@ export class DebugThread extends DebugThreadData implements TreeElement { return this.session.id + ':' + this.raw.id; } + get threadId(): number { + return this.raw.id; + } + protected _currentFrame: DebugStackFrame | undefined; get currentFrame(): DebugStackFrame | undefined { return this._currentFrame; } set currentFrame(frame: DebugStackFrame | undefined) { + if (this._currentFrame?.id === frame?.id) { + return; + } this._currentFrame = frame; this.onDidChangedEmitter.fire(undefined); this.onDidFocusStackFrameEmitter.fire(frame); diff --git a/packages/debug/src/browser/view/debug-threads-widget.ts b/packages/debug/src/browser/view/debug-threads-widget.ts index 7933a84352ad1..aa70d04ec97f9 100644 --- a/packages/debug/src/browser/view/debug-threads-widget.ts +++ b/packages/debug/src/browser/view/debug-threads-widget.ts @@ -99,6 +99,7 @@ export class DebugThreadsWidget extends SourceTreeWidget { this.viewModel.currentSession = node.element; this.debugCallStackItemTypeKey.set('session'); } else if (node.element instanceof DebugThread) { + this.viewModel.currentSession = node.element.session; node.element.session.currentThread = node.element; this.debugCallStackItemTypeKey.set('thread'); } diff --git a/packages/debug/src/common/debug-configuration.ts b/packages/debug/src/common/debug-configuration.ts index b441a39416abe..63efae4a8a7f3 100644 --- a/packages/debug/src/common/debug-configuration.ts +++ b/packages/debug/src/common/debug-configuration.ts @@ -100,6 +100,10 @@ export interface DebugSessionOptions { suppressSaveBeforeStart?: boolean; suppressDebugStatusbar?: boolean; suppressDebugView?: boolean; + testRun?: { + controllerId: string, + runId: string + } } export enum DebugConsoleMode { diff --git a/packages/debug/src/node/debug-adapter-session-manager.ts b/packages/debug/src/node/debug-adapter-session-manager.ts index ff4290e73eae7..4f5501b120038 100644 --- a/packages/debug/src/node/debug-adapter-session-manager.ts +++ b/packages/debug/src/node/debug-adapter-session-manager.ts @@ -37,7 +37,7 @@ export class DebugAdapterSessionManager implements MessagingService.Contribution protected readonly debugAdapterFactory: DebugAdapterFactory; configure(service: MessagingService): void { - service.wsChannel(`${DebugAdapterPath}/:id`, ({ id }: { id: string }, wsChannel) => { + service.registerChannelHandler(`${DebugAdapterPath}/:id`, ({ id }: { id: string }, wsChannel) => { const session = this.find(id); if (!session) { wsChannel.close(); diff --git a/packages/debug/tsconfig.json b/packages/debug/tsconfig.json index aec95d2fcfb8e..a87f1347f8519 100644 --- a/packages/debug/tsconfig.json +++ b/packages/debug/tsconfig.json @@ -39,6 +39,9 @@ { "path": "../terminal" }, + { + "path": "../test" + }, { "path": "../variable-resolver" }, diff --git a/packages/dev-container/.eslintrc.js b/packages/dev-container/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/dev-container/.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/dev-container/README.md b/packages/dev-container/README.md new file mode 100644 index 0000000000000..b9ce2d06af2e5 --- /dev/null +++ b/packages/dev-container/README.md @@ -0,0 +1,43 @@ +
    + +
    + +theia-ext-logo + +

    ECLIPSE THEIA - DEV-CONTAINER EXTENSION

    + +
    + +
    + +## Description + +The `@theia/dev-container` extension provides functionality to create, start and connect to development containers similiar to the +[vscode Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). + +The full devcontainer.json Schema can be found [here](https://containers.dev/implementors/json_reference/). +Currently only a small number of configuration file properties are implemented. Those include the following: +- name +- Image +- dockerfile/build.dockerfile +- build.context +- location +- forwardPorts +- mounts + +see `main-container-creation-contributions.ts` for how to implementations or how to implement additional ones. + + +## 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/dev-container/package.json b/packages/dev-container/package.json new file mode 100644 index 0000000000000..8c50696c150f6 --- /dev/null +++ b/packages/dev-container/package.json @@ -0,0 +1,54 @@ +{ + "name": "@theia/dev-container", + "version": "1.54.0", + "description": "Theia - Editor Preview Extension", + "dependencies": { + "@theia/core": "1.54.0", + "@theia/output": "1.54.0", + "@theia/remote": "1.54.0", + "@theia/workspace": "1.54.0", + "dockerode": "^4.0.2", + "jsonc-parser": "^2.2.0", + "uuid": "^8.0.0" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontendElectron": "lib/electron-browser/dev-container-frontend-module", + "backendElectron": "lib/electron-node/dev-container-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" + ], + "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", + "@types/dockerode": "^3.3.23" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/dev-container/src/dev-container-server/dev-container-server.ts b/packages/dev-container/src/dev-container-server/dev-container-server.ts new file mode 100644 index 0000000000000..0e477150539a3 --- /dev/null +++ b/packages/dev-container/src/dev-container-server/dev-container-server.ts @@ -0,0 +1,53 @@ +// ***************************************************************************** +// 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 { createConnection } from 'net'; +import { stdin, argv, stdout } from 'process'; + +/** + * this node.js Program is supposed to be executed by an docker exec session inside a docker container. + * It uses a tty session to listen on stdin and send on stdout all communication with the theia backend running inside the container. + */ + +let backendPort: number | undefined = undefined; +argv.slice(2).forEach(arg => { + if (arg.startsWith('-target-port')) { + backendPort = parseInt(arg.split('=')[1]); + } +}); + +if (!backendPort) { + throw new Error('please start with -target-port={port number}'); +} +if (stdin.isTTY) { + stdin.setRawMode(true); +} +const connection = createConnection(backendPort, '0.0.0.0'); + +connection.pipe(stdout); +stdin.pipe(connection); + +connection.on('error', error => { + console.error('connection error', error); +}); + +connection.on('close', () => { + console.log('connection closed'); + process.exit(0); +}); + +// keep the process running +setInterval(() => { }, 1 << 30); diff --git a/packages/dev-container/src/electron-browser/container-connection-contribution.ts b/packages/dev-container/src/electron-browser/container-connection-contribution.ts new file mode 100644 index 0000000000000..415ada831f733 --- /dev/null +++ b/packages/dev-container/src/electron-browser/container-connection-contribution.ts @@ -0,0 +1,103 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { AbstractRemoteRegistryContribution, RemoteRegistry } from '@theia/remote/lib/electron-browser/remote-registry-contribution'; +import { LastContainerInfo, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider'; +import { RemotePreferences } from '@theia/remote/lib/electron-browser/remote-preferences'; +import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service'; +import { Command, QuickInputService } from '@theia/core'; +import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; +import { ContainerOutputProvider } from './container-output-provider'; + +export namespace RemoteContainerCommands { + export const REOPEN_IN_CONTAINER = Command.toLocalizedCommand({ + id: 'dev-container:reopen-in-container', + label: 'Reopen in Container', + category: 'Dev Container' + }, 'theia/dev-container/connect'); +} + +const LAST_USED_CONTAINER = 'lastUsedContainer'; +@injectable() +export class ContainerConnectionContribution extends AbstractRemoteRegistryContribution { + + @inject(RemoteContainerConnectionProvider) + protected readonly connectionProvider: RemoteContainerConnectionProvider; + + @inject(RemotePreferences) + protected readonly remotePreferences: RemotePreferences; + + @inject(WorkspaceStorageService) + protected readonly workspaceStorageService: WorkspaceStorageService; + + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(ContainerOutputProvider) + protected readonly containerOutputProvider: ContainerOutputProvider; + + registerRemoteCommands(registry: RemoteRegistry): void { + registry.registerCommand(RemoteContainerCommands.REOPEN_IN_CONTAINER, { + execute: () => this.openInContainer() + }); + } + + async openInContainer(): Promise { + const devcontainerFile = await this.getOrSelectDevcontainerFile(); + if (!devcontainerFile) { + return; + } + const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile}`; + const lastContainerInfo = await this.workspaceStorageService.getData(lastContainerInfoKey); + + this.containerOutputProvider.openChannel(); + + const connectionResult = await this.connectionProvider.connectToContainer({ + nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'], + lastContainerInfo, + devcontainerFile + }); + + this.workspaceStorageService.setData(lastContainerInfoKey, { + id: connectionResult.containerId, + lastUsed: Date.now() + }); + + this.openRemote(connectionResult.port, false, connectionResult.workspacePath); + } + + async getOrSelectDevcontainerFile(): Promise { + const devcontainerFiles = await this.connectionProvider.getDevContainerFiles(); + + if (devcontainerFiles.length === 1) { + return devcontainerFiles[0].path; + } + + return (await this.quickInputService.pick(devcontainerFiles.map(file => ({ + type: 'item', + label: file.name, + description: file.path, + file: file.path, + })), { + title: 'Select a devcontainer.json file' + }))?.file; + } + +} diff --git a/packages/dev-container/src/electron-browser/container-info-contribution.ts b/packages/dev-container/src/electron-browser/container-info-contribution.ts new file mode 100644 index 0000000000000..06a39417f5a6b --- /dev/null +++ b/packages/dev-container/src/electron-browser/container-info-contribution.ts @@ -0,0 +1,46 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import type { ContainerInspectInfo } from 'dockerode'; +import { RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider'; +import { PortForwardingService } from '@theia/remote/lib/electron-browser/port-forwarding/port-forwarding-service'; + +@injectable() +export class ContainerInfoContribution implements FrontendApplicationContribution { + + @inject(RemoteContainerConnectionProvider) + protected readonly connectionProvider: RemoteContainerConnectionProvider; + + @inject(PortForwardingService) + protected readonly portForwardingService: PortForwardingService; + + containerInfo: ContainerInspectInfo | undefined; + + async onStart(): Promise { + this.containerInfo = await this.connectionProvider.getCurrentContainerInfo(parseInt(new URLSearchParams(location.search).get('port') ?? '0')); + + this.portForwardingService.forwardedPorts = Object.entries(this.containerInfo?.NetworkSettings.Ports ?? {}).flatMap(([_, ports]) => ( + ports.map(port => ({ + editing: false, + address: port.HostIp ?? '', + localPort: parseInt(port.HostPort ?? '0'), + origin: 'container' + })))); + } + +} diff --git a/packages/dev-container/src/electron-browser/container-output-provider.ts b/packages/dev-container/src/electron-browser/container-output-provider.ts new file mode 100644 index 0000000000000..f0891c943d085 --- /dev/null +++ b/packages/dev-container/src/electron-browser/container-output-provider.ts @@ -0,0 +1,36 @@ +// ***************************************************************************** +// 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 { injectable, inject } from '@theia/core/shared/inversify'; +import { OutputChannel, OutputChannelManager } from '@theia/output/lib/browser/output-channel'; + +@injectable() +export class ContainerOutputProvider implements ContainerOutputProvider { + + @inject(OutputChannelManager) + protected readonly outputChannelManager: OutputChannelManager; + + protected currentChannel?: OutputChannel; + + openChannel(): void { + this.currentChannel = this.outputChannelManager.getChannel('Container'); + this.currentChannel.show(); + }; + + onRemoteOutput(output: string): void { + this.currentChannel?.appendLine(output); + } +} diff --git a/packages/dev-container/src/electron-browser/dev-container-frontend-module.ts b/packages/dev-container/src/electron-browser/dev-container-frontend-module.ts new file mode 100644 index 0000000000000..807e5152cb2d9 --- /dev/null +++ b/packages/dev-container/src/electron-browser/dev-container-frontend-module.ts @@ -0,0 +1,38 @@ +// ***************************************************************************** +// 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { RemoteRegistryContribution } from '@theia/remote/lib/electron-browser/remote-registry-contribution'; +import { RemoteContainerConnectionProvider, RemoteContainerConnectionProviderPath } from '../electron-common/remote-container-connection-provider'; +import { ContainerConnectionContribution } from './container-connection-contribution'; +import { ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider'; +import { ContainerOutputProvider } from './container-output-provider'; +import { ContainerInfoContribution } from './container-info-contribution'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; + +export default new ContainerModule(bind => { + bind(ContainerConnectionContribution).toSelf().inSingletonScope(); + bind(RemoteRegistryContribution).toService(ContainerConnectionContribution); + + bind(ContainerOutputProvider).toSelf().inSingletonScope(); + + bind(RemoteContainerConnectionProvider).toDynamicValue(ctx => { + const outputProvider = ctx.container.get(ContainerOutputProvider); + return ServiceConnectionProvider.createLocalProxy(ctx.container, RemoteContainerConnectionProviderPath, outputProvider); + }).inSingletonScope(); + + bind(ContainerInfoContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(ContainerInfoContribution); +}); diff --git a/packages/dev-container/src/electron-common/container-output-provider.ts b/packages/dev-container/src/electron-common/container-output-provider.ts new file mode 100644 index 0000000000000..ae7d1b712e788 --- /dev/null +++ b/packages/dev-container/src/electron-common/container-output-provider.ts @@ -0,0 +1,19 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export interface ContainerOutputProvider { + onRemoteOutput(output: string): void; +} diff --git a/packages/dev-container/src/electron-common/remote-container-connection-provider.ts b/packages/dev-container/src/electron-common/remote-container-connection-provider.ts new file mode 100644 index 0000000000000..c6674f91371b9 --- /dev/null +++ b/packages/dev-container/src/electron-common/remote-container-connection-provider.ts @@ -0,0 +1,51 @@ +// ***************************************************************************** +// 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 { RpcServer } from '@theia/core'; +import { ContainerOutputProvider } from './container-output-provider'; +import type { ContainerInspectInfo } from 'dockerode'; + +// ***************************************************************************** +export const RemoteContainerConnectionProviderPath = '/remote/container'; + +export const RemoteContainerConnectionProvider = Symbol('RemoteContainerConnectionProvider'); + +export interface ContainerConnectionOptions { + nodeDownloadTemplate?: string; + lastContainerInfo?: LastContainerInfo + devcontainerFile: string; +} + +export interface LastContainerInfo { + id: string; + lastUsed: number; +} + +export interface ContainerConnectionResult { + port: string; + workspacePath: string; + containerId: string; +} + +export interface DevContainerFile { + name: string; + path: string; +} + +export interface RemoteContainerConnectionProvider extends RpcServer { + connectToContainer(options: ContainerConnectionOptions): Promise; + getDevContainerFiles(): Promise; + getCurrentContainerInfo(port: number): Promise; +} diff --git a/packages/dev-container/src/electron-node/dev-container-backend-module.ts b/packages/dev-container/src/electron-node/dev-container-backend-module.ts new file mode 100644 index 0000000000000..e03066275fba8 --- /dev/null +++ b/packages/dev-container/src/electron-node/dev-container-backend-module.ts @@ -0,0 +1,58 @@ +// ***************************************************************************** +// 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module'; +import { DevContainerConnectionProvider } from './remote-container-connection-provider'; +import { RemoteContainerConnectionProvider, RemoteContainerConnectionProviderPath } from '../electron-common/remote-container-connection-provider'; +import { ContainerCreationContribution, DockerContainerService } from './docker-container-service'; +import { bindContributionProvider, ConnectionHandler, RpcConnectionHandler } from '@theia/core'; +import { registerContainerCreationContributions } from './devcontainer-contributions/main-container-creation-contributions'; +import { DevContainerFileService } from './dev-container-file-service'; +import { ContainerOutputProvider } from '../electron-common/container-output-provider'; +import { ExtensionsContribution, registerTheiaStartOptionsContributions, SettingsContribution } from './devcontainer-contributions/cli-enhancing-creation-contributions'; +import { RemoteCliContribution } from '@theia/core/lib/node/remote/remote-cli-contribution'; +import { ProfileFileModificationContribution } from './devcontainer-contributions/profile-file-modification-contribution'; + +export const remoteConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { + bindContributionProvider(bind, ContainerCreationContribution); + registerContainerCreationContributions(bind); + registerTheiaStartOptionsContributions(bind); + bind(ProfileFileModificationContribution).toSelf().inSingletonScope(); + bind(ContainerCreationContribution).toService(ProfileFileModificationContribution); + + bind(DevContainerConnectionProvider).toSelf().inSingletonScope(); + bind(RemoteContainerConnectionProvider).toService(DevContainerConnectionProvider); + bind(ConnectionHandler).toDynamicValue(ctx => + new RpcConnectionHandler(RemoteContainerConnectionProviderPath, client => { + const server = ctx.container.get(RemoteContainerConnectionProvider); + server.setClient(client); + client.onDidCloseConnection(() => server.dispose()); + return server; + })); +}); + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(DockerContainerService).toSelf().inSingletonScope(); + bind(ConnectionContainerModule).toConstantValue(remoteConnectionModule); + + bind(DevContainerFileService).toSelf().inSingletonScope(); + + bind(ExtensionsContribution).toSelf().inSingletonScope(); + bind(SettingsContribution).toSelf().inSingletonScope(); + bind(RemoteCliContribution).toService(ExtensionsContribution); + bind(RemoteCliContribution).toService(SettingsContribution); +}); diff --git a/packages/dev-container/src/electron-node/dev-container-file-service.ts b/packages/dev-container/src/electron-node/dev-container-file-service.ts new file mode 100644 index 0000000000000..00f59186f7710 --- /dev/null +++ b/packages/dev-container/src/electron-node/dev-container-file-service.ts @@ -0,0 +1,72 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { WorkspaceServer } from '@theia/workspace/lib/common'; +import { DevContainerFile } from '../electron-common/remote-container-connection-provider'; +import { DevContainerConfiguration } from './devcontainer-file'; +import { parse } from 'jsonc-parser'; +import * as fs from '@theia/core/shared/fs-extra'; +import { Path, URI } from '@theia/core'; + +@injectable() +export class DevContainerFileService { + + @inject(WorkspaceServer) + protected readonly workspaceServer: WorkspaceServer; + + async getConfiguration(path: string): Promise { + const configuration: DevContainerConfiguration = parse(await fs.readFile(path, 'utf-8').catch(() => '0')) as DevContainerConfiguration; + if (!configuration) { + throw new Error(`devcontainer file ${path} could not be parsed`); + } + + configuration.location = path; + return configuration; + } + + async getAvailableFiles(): Promise { + const workspace = await this.workspaceServer.getMostRecentlyUsedWorkspace(); + if (!workspace) { + return []; + } + + const devcontainerPath = new URI(workspace).path.join('.devcontainer').fsPath(); + + return (await this.searchForDevontainerJsonFiles(devcontainerPath, 1)).map(file => ({ + name: parse(fs.readFileSync(file, 'utf-8')).name ?? 'devcontainer', + path: file + })); + + } + + protected async searchForDevontainerJsonFiles(directory: string, depth: number): Promise { + if (depth < 0) { + return []; + } + const filesPaths = (await fs.readdir(directory)).map(file => new Path(directory).join(file).fsPath()); + + const devcontainerFiles = []; + for (const file of filesPaths) { + if (file.endsWith('devcontainer.json')) { + devcontainerFiles.push(file); + } else if ((await fs.stat(file)).isDirectory()) { + devcontainerFiles.push(...await this.searchForDevontainerJsonFiles(file, depth - 1)); + } + } + return devcontainerFiles; + } +} diff --git a/packages/dev-container/src/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.ts b/packages/dev-container/src/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.ts new file mode 100644 index 0000000000000..7410d83ea1d06 --- /dev/null +++ b/packages/dev-container/src/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.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 { RemoteCliContext, RemoteCliContribution } from '@theia/core/lib/node/remote/remote-cli-contribution'; +import { ContainerCreationContribution } from '../docker-container-service'; +import * as Docker from 'dockerode'; +import { DevContainerConfiguration, } from '../devcontainer-file'; +import { injectable, interfaces } from '@theia/core/shared/inversify'; + +export function registerTheiaStartOptionsContributions(bind: interfaces.Bind): void { + bind(ContainerCreationContribution).toService(ExtensionsContribution); + bind(ContainerCreationContribution).toService(SettingsContribution); +} + +@injectable() +export class ExtensionsContribution implements RemoteCliContribution, ContainerCreationContribution { + protected currentConfig: DevContainerConfiguration | undefined; + + enhanceArgs(context: RemoteCliContext): string[] { + if (!this.currentConfig) { + return []; + } + const extensions = [ + ...(this.currentConfig.extensions ?? []), + ...(this.currentConfig.customizations?.vscode?.extensions ?? []) + ]; + this.currentConfig = undefined; + return extensions?.map(extension => `--install-plugin=${extension}`); + } + + async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration): Promise { + this.currentConfig = containerConfig; + } +} + +@injectable() +export class SettingsContribution implements RemoteCliContribution, ContainerCreationContribution { + protected currentConfig: DevContainerConfiguration | undefined; + + enhanceArgs(context: RemoteCliContext): string[] { + if (!this.currentConfig) { + return []; + } + const settings = { + ...(this.currentConfig.settings ?? {}), + ...(this.currentConfig.customizations?.vscode?.settings ?? []) + }; + this.currentConfig = undefined; + return Object.entries(settings).map(([key, value]) => `--set-preference=${key}=${JSON.stringify(value)}`) ?? []; + } + + async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration): Promise { + this.currentConfig = containerConfig; + } +} diff --git a/packages/dev-container/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts b/packages/dev-container/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts new file mode 100644 index 0000000000000..ad0d0d4d265de --- /dev/null +++ b/packages/dev-container/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts @@ -0,0 +1,191 @@ +// ***************************************************************************** +// 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 * as Docker from 'dockerode'; +import { inject, injectable, interfaces } from '@theia/core/shared/inversify'; +import { ContainerCreationContribution } from '../docker-container-service'; +import { DevContainerConfiguration, DockerfileContainer, ImageContainer, NonComposeContainerBase } from '../devcontainer-file'; +import { Path } from '@theia/core'; +import { ContainerOutputProvider } from '../../electron-common/container-output-provider'; +import * as fs from '@theia/core/shared/fs-extra'; +import { RemotePortForwardingProvider } from '@theia/remote/lib/electron-common/remote-port-forwarding-provider'; +import { RemoteDockerContainerConnection } from '../remote-container-connection-provider'; + +export function registerContainerCreationContributions(bind: interfaces.Bind): void { + bind(ContainerCreationContribution).to(ImageFileContribution).inSingletonScope(); + bind(ContainerCreationContribution).to(DockerFileContribution).inSingletonScope(); + bind(ContainerCreationContribution).to(ForwardPortsContribution).inSingletonScope(); + bind(ContainerCreationContribution).to(MountsContribution).inSingletonScope(); + bind(ContainerCreationContribution).to(RemoteUserContribution).inSingletonScope(); + bind(ContainerCreationContribution).to(PostCreateCommandContribution).inSingletonScope(); +} + +@injectable() +export class ImageFileContribution implements ContainerCreationContribution { + async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: ImageContainer, + api: Docker, outputprovider: ContainerOutputProvider): Promise { + if (containerConfig.image) { + await new Promise((res, rej) => api.pull(containerConfig.image, {}, (err, stream) => { + if (err) { + rej(err); + } else { + api.modem.followProgress(stream, (error, output) => error ? + rej(error) : + res(), progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress))); + } + })); + createOptions.Image = containerConfig.image; + } + } +} + +@injectable() +export class DockerFileContribution implements ContainerCreationContribution { + async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DockerfileContainer, + api: Docker, outputprovider: ContainerOutputProvider): Promise { + // check if dockerfile container + if (containerConfig.dockerFile || containerConfig.build?.dockerfile) { + const dockerfile = (containerConfig.dockerFile ?? containerConfig.build?.dockerfile) as string; + const context = containerConfig.context ?? new Path(containerConfig.location as string).dir.fsPath(); + try { + // ensure dockerfile exists + await fs.lstat(new Path(context as string).join(dockerfile).fsPath()); + + const buildStream = await api.buildImage({ + context, + src: [dockerfile], + } as Docker.ImageBuildContext, { + buildargs: containerConfig.build?.args + }); + // TODO probably have some console windows showing the output of the build + const imageId = await new Promise((res, rej) => api.modem.followProgress(buildStream!, (err, outputs) => { + if (err) { + rej(err); + } else { + for (let i = outputs.length - 1; i >= 0; i--) { + if (outputs[i].aux?.ID) { + res(outputs[i].aux.ID); + return; + } + } + } + }, progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress)))); + createOptions.Image = imageId; + } catch (error) { + outputprovider.onRemoteOutput(`could not build dockerfile "${dockerfile}" reason: ${error.message}`); + throw error; + } + } + } +} + +@injectable() +export class ForwardPortsContribution implements ContainerCreationContribution { + + @inject(RemotePortForwardingProvider) + protected readonly portForwardingProvider: RemotePortForwardingProvider; + + async handlePostConnect(containerConfig: DevContainerConfiguration, connection: RemoteDockerContainerConnection): Promise { + if (!containerConfig.forwardPorts) { + return; + } + + for (const forward of containerConfig.forwardPorts) { + let port: number; + let address: string | undefined; + if (typeof forward === 'string') { + const parts = forward.split(':'); + address = parts[0]; + port = parseInt(parts[1]); + } else { + port = forward; + } + + this.portForwardingProvider.forwardPort(connection.localPort, { port, address }); + } + + } + +} + +@injectable() +export class MountsContribution implements ContainerCreationContribution { + async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration, api: Docker): Promise { + if (!containerConfig.mounts) { + return; + } + + createOptions.HostConfig!.Mounts!.push(...(containerConfig as NonComposeContainerBase)?.mounts + ?.map(mount => typeof mount === 'string' ? + this.parseMountString(mount) : + { Source: mount.source, Target: mount.target, Type: mount.type ?? 'bind' }) ?? []); + } + + parseMountString(mount: string): Docker.MountSettings { + const parts = mount.split(','); + return { + Source: parts.find(part => part.startsWith('source=') || part.startsWith('src='))?.split('=')[1]!, + Target: parts.find(part => part.startsWith('target=') || part.startsWith('dst='))?.split('=')[1]!, + Type: (parts.find(part => part.startsWith('type='))?.split('=')[1] ?? 'bind') as Docker.MountType + }; + } +} + +@injectable() +export class RemoteUserContribution implements ContainerCreationContribution { + async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration, api: Docker): Promise { + if (containerConfig.remoteUser) { + createOptions.User = containerConfig.remoteUser; + } + } +} + +@injectable() +export class PostCreateCommandContribution implements ContainerCreationContribution { + async handlePostCreate?(containerConfig: DevContainerConfiguration, container: Docker.Container, api: Docker, outputprovider: ContainerOutputProvider): Promise { + if (containerConfig.postCreateCommand) { + const commands = typeof containerConfig.postCreateCommand === 'object' && !(containerConfig.postCreateCommand instanceof Array) ? + Object.values(containerConfig.postCreateCommand) : [containerConfig.postCreateCommand]; + for (const command of commands) { + try { + let exec; + if (command instanceof Array) { + exec = await container.exec({ Cmd: command, AttachStderr: true, AttachStdout: true }); + + } else { + exec = await container.exec({ Cmd: ['sh', '-c', command], AttachStderr: true, AttachStdout: true }); + } + const stream = await exec.start({ Tty: true }); + stream.on('data', chunk => outputprovider.onRemoteOutput(chunk.toString())); + } catch (error) { + outputprovider.onRemoteOutput('could not execute postCreateCommand ' + JSON.stringify(command) + ' reason:' + error.message); + } + } + } + } +} + +export namespace OutputHelper { + export interface Progress { + id?: string; + stream: string; + status?: string; + progress?: string; + } + + export function parseProgress(progress: Progress): string { + return progress.stream ?? progress.progress ?? progress.status ?? ''; + } +} diff --git a/packages/dev-container/src/electron-node/devcontainer-contributions/profile-file-modification-contribution.ts b/packages/dev-container/src/electron-node/devcontainer-contributions/profile-file-modification-contribution.ts new file mode 100644 index 0000000000000..6cfa4033c0de5 --- /dev/null +++ b/packages/dev-container/src/electron-node/devcontainer-contributions/profile-file-modification-contribution.ts @@ -0,0 +1,35 @@ +// ***************************************************************************** +// 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 { DevContainerConfiguration } from '../devcontainer-file'; +import { ContainerCreationContribution } from '../docker-container-service'; +import * as Docker from 'dockerode'; +import { injectable } from '@theia/core/shared/inversify'; +import { ContainerOutputProvider } from '../../electron-common/container-output-provider'; + +/** + * this contribution changes the /etc/profile file so that it won't overwrite the PATH variable set by docker + */ +@injectable() +export class ProfileFileModificationContribution implements ContainerCreationContribution { + async handlePostCreate(containerConfig: DevContainerConfiguration, container: Docker.Container, api: Docker, outputprovider: ContainerOutputProvider): Promise { + const stream = await (await container.exec({ + Cmd: ['sh', '-c', 'sed -i \'s|PATH="\\([^"]*\\)"|PATH=${PATH:-"\\1"}|g\' /etc/profile'], User: 'root', + AttachStderr: true, AttachStdout: true + })).start({}); + stream.on('data', data => outputprovider.onRemoteOutput(data.toString())); + } +} diff --git a/packages/dev-container/src/electron-node/devcontainer-file.ts b/packages/dev-container/src/electron-node/devcontainer-file.ts new file mode 100644 index 0000000000000..ec77ac4b5e7f3 --- /dev/null +++ b/packages/dev-container/src/electron-node/devcontainer-file.ts @@ -0,0 +1,411 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/** + * Defines a dev container + * type generated from https://containers.dev/implementors/json_schema/ and modified + */ +export type DevContainerConfiguration = (((DockerfileContainer | ImageContainer) & (NonComposeContainerBase)) | ComposeContainer) & DevContainerCommon & { location?: string }; + +export type DockerfileContainer = { + /** + * Docker build-related options. + */ + build: { + /** + * The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file. + */ + dockerfile: string + /** + * The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file. + */ + context?: string + } & BuildOptions + [k: string]: unknown +} | { + /** + * The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file. + */ + dockerFile: string + /** + * The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file. + */ + context?: string + + /** + * Docker build-related options. + */ + build?: { + /** + * Target stage in a multi-stage build. + */ + target?: string + /** + * Build arguments. + */ + args?: { + [k: string]: string + } + /** + * The image to consider as a cache. Use an array to specify multiple images. + */ + cacheFrom?: string | string[] + [k: string]: unknown + } + [k: string]: unknown +}; + +export interface BuildOptions { + /** + * Target stage in a multi-stage build. + */ + target?: string + /** + * Build arguments. + */ + args?: { + [k: string]: string + } + /** + * The image to consider as a cache. Use an array to specify multiple images. + */ + cacheFrom?: string | string[] + [k: string]: unknown +} +export interface ImageContainer { + /** + * The docker image that will be used to create the container. + */ + image: string + [k: string]: unknown +} + +export interface NonComposeContainerBase { + /** + * Application ports that are exposed by the container. This can be a single port or an array of ports. Each port can be a number or a string. A number is mapped to + * the same port on the host. A string is passed to Docker unchanged and can be used to map ports differently, e.g. '8000:8010'. + */ + appPort?: number | string | (number | string)[] + /** + * Container environment variables. + */ + containerEnv?: { + [k: string]: string + } + /** + * The user the container will be started with. The default is the user on the Docker image. + */ + containerUser?: string + /** + * Mount points to set up when creating the container. See Docker's documentation for the --mount option for the supported syntax. + */ + mounts?: (string | MountConfig)[] + /** + * The arguments required when starting in the container. + */ + runArgs?: string[] + /** + * Action to take when the user disconnects from the container in their editor. The default is to stop the container. + */ + shutdownAction?: 'none' | 'stopContainer' + /** + * Whether to overwrite the command specified in the image. The default is true. + */ + overrideCommand?: boolean + /** + * The path of the workspace folder inside the container. + */ + workspaceFolder?: string + /** + * The --mount parameter for docker run. The default is to mount the project folder at /workspaces/$project. + */ + workspaceMount?: string + [k: string]: unknown +} + +export interface ComposeContainer { + /** + * The name of the docker-compose file(s) used to start the services. + */ + dockerComposeFile: string | string[] + /** + * The service you want to work on. This is considered the primary container for your dev environment which your editor will connect to. + */ + service: string + /** + * An array of services that should be started and stopped. + */ + runServices?: string[] + /** + * The path of the workspace folder inside the container. This is typically the target path of a volume mount in the docker-compose.yml. + */ + workspaceFolder: string + /** + * Action to take when the user disconnects from the primary container in their editor. The default is to stop all of the compose containers. + */ + shutdownAction?: 'none' | 'stopCompose' + /** + * Whether to overwrite the command specified in the image. The default is false. + */ + overrideCommand?: boolean + [k: string]: unknown +} + +export interface DevContainerCommon { + /** + * A name for the dev container which can be displayed to the user. + */ + name?: string + /** + * Features to add to the dev container. + */ + features?: { + [k: string]: unknown + } + /** + * Array consisting of the Feature id (without the semantic version) of Features in the order the user wants them to be installed. + */ + overrideFeatureInstallOrder?: string[] + /** + * Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format 'host:port_number'. + */ + forwardPorts?: (number | string)[] + portsAttributes?: { + /** + * A port, range of ports (ex. '40000-55000'), or regular expression (ex. '.+\\/server.js'). + * For a port number or range, the attributes will apply to that port number or range of port numbers. + * Attributes which use a regular expression will apply to ports whose associated process command line matches the expression. + * + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` '(^\d+(-\d+)?$)|(.+)'. + */ + [k: string]: { + /** + * Defines the action that occurs when the port is discovered for automatic forwarding + */ + onAutoForward?: + | 'notify' + | 'openBrowser' + | 'openBrowserOnce' + | 'openPreview' + | 'silent' + | 'ignore' + /** + * Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port. + */ + elevateIfNeeded?: boolean + /** + * Label that will be shown in the UI for this port. + */ + label?: string + requireLocalPort?: boolean + /** + * The protocol to use when forwarding this port. + */ + protocol?: 'http' | 'https' + [k: string]: unknown + } + } + otherPortsAttributes?: { + /** + * Defines the action that occurs when the port is discovered for automatic forwarding + */ + onAutoForward?: + | 'notify' + | 'openBrowser' + | 'openPreview' + | 'silent' + | 'ignore' + /** + * Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port. + */ + elevateIfNeeded?: boolean + /** + * Label that will be shown in the UI for this port. + */ + label?: string + requireLocalPort?: boolean + /** + * The protocol to use when forwarding this port. + */ + protocol?: 'http' | 'https' + } + /** + * Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder. + */ + updateRemoteUserUID?: boolean + /** + * Remote environment variables to set for processes spawned in the container including lifecycle scripts and any remote editor/IDE server process. + */ + remoteEnv?: { + [k: string]: string | null + } + /** + * The username to use for spawning processes in the container including lifecycle scripts and any remote editor/IDE server process. + * The default is the same user as the container. + */ + remoteUser?: string + + /** + * extensions to install in the container at launch. The expeceted format is publisher.name[@version]. + * The default is no extensions being installed. + */ + extensions?: string[] + + /** + * settings to set in the container at launch in the settings.json. The expected format is key=value. + * The default is no preferences being set. + */ + settings?: { + [k: string]: unknown + } + + /** + * A command to run locally before anything else. This command is run before 'onCreateCommand'. + * If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. + */ + initializeCommand?: string | string[] + /** + * A command to run when creating the container. This command is run after 'initializeCommand' and before 'updateContentCommand'. + * If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. + */ + onCreateCommand?: + | string + | string[] + | { + [k: string]: string | string[] + } + /** + * A command to run when creating the container and rerun when the workspace content was updated while creating the container. + * This command is run after 'onCreateCommand' and before 'postCreateCommand'. If this is a single string, it will be run in a shell. + * If this is an array of strings, it will be run as a single command without shell. + */ + updateContentCommand?: + | string + | string[] + | { + [k: string]: string | string[] + } + /** + * A command to run after creating the container. This command is run after 'updateContentCommand' and before 'postStartCommand'. + * If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. + */ + postCreateCommand?: + | string + | string[] + | { + [k: string]: string | string[] + } + /** + * A command to run after starting the container. This command is run after 'postCreateCommand' and before 'postAttachCommand'. + * If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. + */ + postStartCommand?: + | string + | string[] + | { + [k: string]: string | string[] + } + /** + * A command to run when attaching to the container. This command is run after 'postStartCommand'. If this is a single string, it will be run in a shell. + * If this is an array of strings, it will be run as a single command without shell. + */ + postAttachCommand?: + | string + | string[] + | { + [k: string]: string | string[] + } + /** + * The user command to wait for before continuing execution in the background while the UI is starting up. The default is 'updateContentCommand'. + */ + waitFor?: + | 'initializeCommand' + | 'onCreateCommand' + | 'updateContentCommand' + | 'postCreateCommand' + | 'postStartCommand' + /** + * User environment probe to run. The default is 'loginInteractiveShell'. + */ + userEnvProbe?: + | 'none' + | 'loginShell' + | 'loginInteractiveShell' + | 'interactiveShell' + /** + * Host hardware requirements. + */ + hostRequirements?: { + /** + * Number of required CPUs. + */ + cpus?: number + /** + * Amount of required RAM in bytes. Supports units tb, gb, mb and kb. + */ + memory?: string + /** + * Amount of required disk space in bytes. Supports units tb, gb, mb and kb. + */ + storage?: string + gpu?: + | (true | false | 'optional') + | { + /** + * Number of required cores. + */ + cores?: number + /** + * Amount of required RAM in bytes. Supports units tb, gb, mb and kb. + */ + memory?: string + } + [k: string]: unknown + } + /** + * Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations. + */ + customizations?: { + [k: string]: unknown, + vscode?: { + /** + * extensions to install in the container at launch. The expeceted format is publisher.name[@version]. + * The default is no extensions being installed. + */ + extensions?: string[], + + /** + * settings to set in the container at launch in the settings.json. The expected format is key=value. + * The default is no preferences being set. + */ + settings?: { + [k: string]: unknown + } + [k: string]: unknown + } + } + additionalProperties?: { + [k: string]: unknown + } + [k: string]: unknown +} + +export interface MountConfig { + source: string, + target: string, + type: 'volume' | 'bind', +} diff --git a/packages/dev-container/src/electron-node/docker-container-service.ts b/packages/dev-container/src/electron-node/docker-container-service.ts new file mode 100644 index 0000000000000..70dfbc2fa72a0 --- /dev/null +++ b/packages/dev-container/src/electron-node/docker-container-service.ts @@ -0,0 +1,132 @@ +// ***************************************************************************** +// 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 { ContributionProvider, MaybePromise, URI } from '@theia/core'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { WorkspaceServer } from '@theia/workspace/lib/common'; +import * as fs from '@theia/core/shared/fs-extra'; +import * as Docker from 'dockerode'; +import { LastContainerInfo } from '../electron-common/remote-container-connection-provider'; +import { DevContainerConfiguration } from './devcontainer-file'; +import { DevContainerFileService } from './dev-container-file-service'; +import { ContainerOutputProvider } from '../electron-common/container-output-provider'; +import { RemoteDockerContainerConnection } from './remote-container-connection-provider'; + +export const ContainerCreationContribution = Symbol('ContainerCreationContributions'); + +export interface ContainerCreationContribution { + handleContainerCreation?(createOptions: Docker.ContainerCreateOptions, + containerConfig: DevContainerConfiguration, + api: Docker, + outputProvider?: ContainerOutputProvider): MaybePromise; + + /** + * executed after creating and starting the container + */ + handlePostCreate?(containerConfig: DevContainerConfiguration, + container: Docker.Container, + api: Docker, + outputProvider?: ContainerOutputProvider): MaybePromise; + + /** + * executed after a connection has been established with the container and theia has been setup + */ + handlePostConnect?(containerConfig: DevContainerConfiguration, connection: RemoteDockerContainerConnection, + outputProvider?: ContainerOutputProvider): MaybePromise; +} + +@injectable() +export class DockerContainerService { + + @inject(WorkspaceServer) + protected readonly workspaceServer: WorkspaceServer; + + @inject(ContributionProvider) @named(ContainerCreationContribution) + protected readonly containerCreationContributions: ContributionProvider; + + @inject(DevContainerFileService) + protected readonly devContainerFileService: DevContainerFileService; + + async getOrCreateContainer(docker: Docker, devcontainerFile: string, + lastContainerInfo?: LastContainerInfo, outputProvider?: ContainerOutputProvider): Promise { + let container; + + const workspace = new URI(await this.workspaceServer.getMostRecentlyUsedWorkspace()); + + if (lastContainerInfo && fs.statSync(devcontainerFile).mtimeMs < lastContainerInfo.lastUsed) { + try { + container = docker.getContainer(lastContainerInfo.id); + if ((await container.inspect()).State.Running) { + await container.restart(); + } else { + await container.start(); + } + } catch (e) { + container = undefined; + console.warn('DevContainer: could not find last used container'); + } + } + if (!container) { + container = await this.buildContainer(docker, devcontainerFile, workspace, outputProvider); + } + return container; + } + + async postConnect(devcontainerFile: string, connection: RemoteDockerContainerConnection, outputProvider?: ContainerOutputProvider): Promise { + const devcontainerConfig = await this.devContainerFileService.getConfiguration(devcontainerFile); + + for (const containerCreateContrib of this.containerCreationContributions.getContributions()) { + await containerCreateContrib.handlePostConnect?.(devcontainerConfig, connection, outputProvider); + } + + } + + protected async buildContainer(docker: Docker, devcontainerFile: string, workspace: URI, outputProvider?: ContainerOutputProvider): Promise { + const devcontainerConfig = await this.devContainerFileService.getConfiguration(devcontainerFile); + + if (!devcontainerConfig) { + // TODO add ability for user to create new config + throw new Error('No devcontainer.json'); + } + + const containerCreateOptions: Docker.ContainerCreateOptions = { + Tty: true, + ExposedPorts: {}, + HostConfig: { + PortBindings: {}, + Mounts: [{ + Source: workspace.path.toString(), + Target: `/workspaces/${workspace.path.name}`, + Type: 'bind' + }], + }, + }; + + for (const containerCreateContrib of this.containerCreationContributions.getContributions()) { + await containerCreateContrib.handleContainerCreation?.(containerCreateOptions, devcontainerConfig, docker, outputProvider); + } + + // TODO add more config + const container = await docker.createContainer(containerCreateOptions); + await container.start(); + + for (const containerCreateContrib of this.containerCreationContributions.getContributions()) { + await containerCreateContrib.handlePostCreate?.(devcontainerConfig, container, docker, outputProvider); + } + + return container; + } +} diff --git a/packages/dev-container/src/electron-node/remote-container-connection-provider.ts b/packages/dev-container/src/electron-node/remote-container-connection-provider.ts new file mode 100644 index 0000000000000..1efa6abd60613 --- /dev/null +++ b/packages/dev-container/src/electron-node/remote-container-connection-provider.ts @@ -0,0 +1,311 @@ +// ***************************************************************************** +// 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 * as net from 'net'; +import { + ContainerConnectionOptions, ContainerConnectionResult, + DevContainerFile, RemoteContainerConnectionProvider +} from '../electron-common/remote-container-connection-provider'; +import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '@theia/remote/lib/electron-node/remote-types'; +import { RemoteSetupResult, RemoteSetupService } from '@theia/remote/lib/electron-node/setup/remote-setup-service'; +import { RemoteConnectionService } from '@theia/remote/lib/electron-node/remote-connection-service'; +import { RemoteProxyServerProvider } from '@theia/remote/lib/electron-node/remote-proxy-server-provider'; +import { Emitter, Event, generateUuid, MessageService, RpcServer } from '@theia/core'; +import { Socket } from 'net'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import * as Docker from 'dockerode'; +import { DockerContainerService } from './docker-container-service'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { WriteStream } from 'tty'; +import { PassThrough } from 'stream'; +import { exec } from 'child_process'; +import { DevContainerFileService } from './dev-container-file-service'; +import { ContainerOutputProvider } from '../electron-common/container-output-provider'; + +@injectable() +export class DevContainerConnectionProvider implements RemoteContainerConnectionProvider, RpcServer { + + @inject(RemoteConnectionService) + protected readonly remoteConnectionService: RemoteConnectionService; + + @inject(RemoteSetupService) + protected readonly remoteSetup: RemoteSetupService; + + @inject(MessageService) + protected readonly messageService: MessageService; + + @inject(RemoteProxyServerProvider) + protected readonly serverProvider: RemoteProxyServerProvider; + + @inject(DockerContainerService) + protected readonly containerService: DockerContainerService; + + @inject(DevContainerFileService) + protected readonly devContainerFileService: DevContainerFileService; + + @inject(RemoteConnectionService) + protected readonly remoteService: RemoteConnectionService; + + protected outputProvider: ContainerOutputProvider | undefined; + + setClient(client: ContainerOutputProvider): void { + this.outputProvider = client; + } + + async connectToContainer(options: ContainerConnectionOptions): Promise { + const dockerConnection = new Docker(); + const version = await dockerConnection.version().catch(() => undefined); + + if (!version) { + this.messageService.error('Docker Daemon is not running'); + throw new Error('Docker is not running'); + } + + // create container + const progress = await this.messageService.showProgress({ + text: 'Creating container', + }); + try { + const container = await this.containerService.getOrCreateContainer(dockerConnection, options.devcontainerFile, options.lastContainerInfo, this.outputProvider); + const devContainerConfig = await this.devContainerFileService.getConfiguration(options.devcontainerFile); + + // create actual connection + const report: RemoteStatusReport = message => progress.report({ message }); + report('Connecting to remote system...'); + + const remote = await this.createContainerConnection(container, dockerConnection, devContainerConfig.name); + const result = await this.remoteSetup.setup({ + connection: remote, + report, + nodeDownloadTemplate: options.nodeDownloadTemplate + }); + remote.remoteSetupResult = result; + + const registration = this.remoteConnectionService.register(remote); + const server = await this.serverProvider.getProxyServer(socket => { + remote.forwardOut(socket); + }); + remote.onDidDisconnect(() => { + server.close(); + registration.dispose(); + }); + const localPort = (server.address() as net.AddressInfo).port; + remote.localPort = localPort; + + await this.containerService.postConnect(options.devcontainerFile, remote, this.outputProvider); + + return { + containerId: container.id, + workspacePath: (await container.inspect()).Mounts[0].Destination, + port: localPort.toString(), + }; + } catch (e) { + this.messageService.error(e.message); + console.error(e); + throw e; + } finally { + progress.cancel(); + } + } + + getDevContainerFiles(): Promise { + return this.devContainerFileService.getAvailableFiles(); + } + + async createContainerConnection(container: Docker.Container, docker: Docker, name?: string): Promise { + return Promise.resolve(new RemoteDockerContainerConnection({ + id: generateUuid(), + name: name ?? 'dev-container', + type: 'Dev Container', + docker, + container, + })); + } + + async getCurrentContainerInfo(port: number): Promise { + const connection = this.remoteConnectionService.getConnectionFromPort(port); + if (!connection || !(connection instanceof RemoteDockerContainerConnection)) { + return undefined; + } + return connection.container.inspect(); + } + + dispose(): void { + + } + +} + +export interface RemoteContainerConnectionOptions { + id: string; + name: string; + type: string; + docker: Docker; + container: Docker.Container; +} + +interface ContainerTerminalSession { + execution: Docker.Exec, + stdout: WriteStream, + stderr: WriteStream, + executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>; +} + +interface ContainerTerminalSession { + execution: Docker.Exec, + stdout: WriteStream, + stderr: WriteStream, + executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>; +} + +export class RemoteDockerContainerConnection implements RemoteConnection { + + id: string; + name: string; + type: string; + localPort: number; + remotePort: number; + + docker: Docker; + container: Docker.Container; + + remoteSetupResult: RemoteSetupResult; + + protected activeTerminalSession: ContainerTerminalSession | undefined; + + protected readonly onDidDisconnectEmitter = new Emitter(); + onDidDisconnect: Event = this.onDidDisconnectEmitter.event; + + constructor(options: RemoteContainerConnectionOptions) { + this.id = options.id; + this.type = options.type; + this.name = options.name; + + this.docker = options.docker; + this.container = options.container; + + this.docker.getEvents({ filters: { container: [this.container.id], event: ['stop'] } }).then(stream => { + stream.on('data', () => this.onDidDisconnectEmitter.fire()); + }); + } + + async forwardOut(socket: Socket, port?: number): Promise { + const node = `${this.remoteSetupResult.nodeDirectory}/bin/node`; + const devContainerServer = `${this.remoteSetupResult.applicationDirectory}/backend/dev-container-server.js`; + try { + const ttySession = await this.container.exec({ + Cmd: ['sh', '-c', `${node} ${devContainerServer} -target-port=${port ?? this.remotePort}`], + AttachStdin: true, AttachStdout: true, AttachStderr: true + }); + + const stream = await ttySession.start({ hijack: true, stdin: true }); + + socket.pipe(stream); + ttySession.modem.demuxStream(stream, socket, socket); + } catch (e) { + console.error(e); + } + } + + async exec(cmd: string, args?: string[], options?: RemoteExecOptions): Promise { + // return (await this.getOrCreateTerminalSession()).executeCommand(cmd, args); + const deferred = new Deferred(); + try { + // TODO add windows container support + const execution = await this.container.exec({ Cmd: ['sh', '-c', `${cmd} ${args?.join(' ') ?? ''}`], AttachStdout: true, AttachStderr: true }); + let stdoutBuffer = ''; + let stderrBuffer = ''; + const stream = await execution?.start({}); + const stdout = new PassThrough(); + stdout.on('data', (chunk: Buffer) => { + stdoutBuffer += chunk.toString(); + }); + const stderr = new PassThrough(); + stderr.on('data', (chunk: Buffer) => { + stderrBuffer += chunk.toString(); + }); + execution.modem.demuxStream(stream, stdout, stderr); + stream?.addListener('close', () => deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer })); + } catch (e) { + deferred.reject(e); + } + return deferred.promise; + } + + async execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options?: RemoteExecOptions): Promise { + const deferred = new Deferred(); + try { + // TODO add windows container support + const execution = await this.container.exec({ Cmd: ['sh', '-c', `${cmd} ${args?.join(' ') ?? ''}`], AttachStdout: true, AttachStderr: true }); + let stdoutBuffer = ''; + let stderrBuffer = ''; + const stream = await execution?.start({}); + stream.on('close', () => { + if (deferred.state === 'unresolved') { + deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer }); + } + }); + const stdout = new PassThrough(); + stdout.on('data', (data: Buffer) => { + if (deferred.state === 'unresolved') { + stdoutBuffer += data.toString(); + + if (tester(stdoutBuffer, stderrBuffer)) { + deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer }); + } + } + }); + const stderr = new PassThrough(); + stderr.on('data', (data: Buffer) => { + if (deferred.state === 'unresolved') { + stderrBuffer += data.toString(); + + if (tester(stdoutBuffer, stderrBuffer)) { + deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer }); + } + } + }); + execution.modem.demuxStream(stream, stdout, stderr); + } catch (e) { + deferred.reject(e); + } + return deferred.promise; + } + + async copy(localPath: string | Buffer | NodeJS.ReadableStream, remotePath: string): Promise { + const deferred = new Deferred(); + const process = exec(`docker cp -qa ${localPath.toString()} ${this.container.id}:${remotePath}`); + + let stderr = ''; + process.stderr?.on('data', data => { + stderr += data.toString(); + }); + process.on('close', code => { + if (code === 0) { + deferred.resolve(); + } else { + deferred.reject(stderr); + } + }); + return deferred.promise; + } + + async dispose(): Promise { + // cant use dockerrode here since this needs to happen on one tick + exec(`docker stop ${this.container.id}`); + } + +} diff --git a/packages/dev-container/src/package.spec.ts b/packages/dev-container/src/package.spec.ts new file mode 100644 index 0000000000000..759142013d5a6 --- /dev/null +++ b/packages/dev-container/src/package.spec.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2017 Ericsson 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 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('dev-container package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/dev-container/tsconfig.json b/packages/dev-container/tsconfig.json new file mode 100644 index 0000000000000..d7f2cbfb079c4 --- /dev/null +++ b/packages/dev-container/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + }, + { + "path": "../output" + }, + { + "path": "../remote" + }, + { + "path": "../workspace" + } + ] +} diff --git a/packages/editor-preview/package.json b/packages/editor-preview/package.json index 973cd587db484..9c572f902b63e 100644 --- a/packages/editor-preview/package.json +++ b/packages/editor-preview/package.json @@ -1,11 +1,12 @@ { "name": "@theia/editor-preview", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Editor Preview Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/navigator": "1.44.0" + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/navigator": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -40,7 +41,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/editor-preview/src/browser/editor-preview-tree-decorator.ts b/packages/editor-preview/src/browser/editor-preview-tree-decorator.ts index 21c6671cee3ca..c5908ef105624 100644 --- a/packages/editor-preview/src/browser/editor-preview-tree-decorator.ts +++ b/packages/editor-preview/src/browser/editor-preview-tree-decorator.ts @@ -30,11 +30,10 @@ import { import { Disposable } from '@theia/core/lib/common'; import { OpenEditorNode } from '@theia/navigator/lib/browser/open-editors-widget/navigator-open-editors-tree-model'; import { EditorPreviewWidget } from './editor-preview-widget'; -import { EditorPreviewManager } from './editor-preview-manager'; @injectable() export class EditorPreviewTreeDecorator implements TreeDecorator, FrontendApplicationContribution { - @inject(EditorPreviewManager) protected readonly editorPreviewManager: EditorPreviewManager; + @inject(ApplicationShell) protected readonly shell: ApplicationShell; readonly id = 'theia-open-editors-file-decorator'; diff --git a/packages/editor/package.json b/packages/editor/package.json index f1028d74c7f00..32d1e2d9d1f7b 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,17 +1,19 @@ { "name": "@theia/editor", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Editor Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/variable-resolver": "1.44.0" + "@theia/core": "1.54.0", + "@theia/variable-resolver": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" }, "theiaExtensions": [ { - "frontend": "lib/browser/editor-frontend-module" + "frontend": "lib/browser/editor-frontend-module", + "secondaryWindow": "lib/browser/editor-frontend-module" } ], "keywords": [ @@ -39,7 +41,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/editor/src/browser/diff-navigator.ts b/packages/editor/src/browser/diff-navigator.ts index 71679f9dea2bf..b9517831a8691 100644 --- a/packages/editor/src/browser/diff-navigator.ts +++ b/packages/editor/src/browser/diff-navigator.ts @@ -17,7 +17,6 @@ import { TextEditor } from './editor'; export interface DiffNavigator { - canNavigate(): boolean; hasNext(): boolean; hasPrevious(): boolean; next(): void; diff --git a/packages/editor/src/browser/editor-command.ts b/packages/editor/src/browser/editor-command.ts index 81e0c1505d02a..80b13412b6ef3 100644 --- a/packages/editor/src/browser/editor-command.ts +++ b/packages/editor/src/browser/editor-command.ts @@ -15,16 +15,13 @@ // ***************************************************************************** import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify'; -import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common'; -import URI from '@theia/core/lib/common/uri'; -import { CommonCommands, PreferenceService, LabelProvider, ApplicationShell, QuickInputService, QuickPickValue, QuickPickItemOrSeparator } from '@theia/core/lib/browser'; +import { CommonCommands, PreferenceService, LabelProvider, ApplicationShell, QuickInputService, QuickPickValue, SaveableService } from '@theia/core/lib/browser'; import { EditorManager } from './editor-manager'; -import { EditorPreferences } from './editor-preferences'; -import { ResourceProvider, MessageService } from '@theia/core'; -import { LanguageService, Language } from '@theia/core/lib/browser/language-service'; +import { CommandContribution, CommandRegistry, Command, ResourceProvider, MessageService, nls } from '@theia/core'; +import { LanguageService } from '@theia/core/lib/browser/language-service'; import { SUPPORTED_ENCODINGS } from '@theia/core/lib/browser/supported-encodings'; import { EncodingMode } from './editor'; -import { nls } from '@theia/core/lib/common/nls'; +import { EditorLanguageQuickPickService } from './editor-language-quick-pick-service'; export namespace EditorCommands { @@ -209,7 +206,8 @@ export namespace EditorCommands { @injectable() export class EditorCommandContribution implements CommandContribution { - public static readonly AUTOSAVE_PREFERENCE: string = 'files.autoSave'; + static readonly AUTOSAVE_PREFERENCE: string = 'files.autoSave'; + static readonly AUTOSAVE_DELAY_PREFERENCE: string = 'files.autoSaveDelay'; @inject(ApplicationShell) protected readonly shell: ApplicationShell; @@ -217,13 +215,14 @@ export class EditorCommandContribution implements CommandContribution { @inject(PreferenceService) protected readonly preferencesService: PreferenceService; - @inject(EditorPreferences) - protected readonly editorPreferences: EditorPreferences; + @inject(SaveableService) + protected readonly saveResourceService: SaveableService; @inject(QuickInputService) @optional() protected readonly quickInputService: QuickInputService; - @inject(MessageService) protected readonly messageService: MessageService; + @inject(MessageService) + protected readonly messageService: MessageService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @@ -237,11 +236,20 @@ export class EditorCommandContribution implements CommandContribution { @inject(ResourceProvider) protected readonly resourceProvider: ResourceProvider; + @inject(EditorLanguageQuickPickService) + protected readonly codeLanguageQuickPickService: EditorLanguageQuickPickService; + @postConstruct() protected init(): void { - this.editorPreferences.onPreferenceChanged(e => { - if (e.preferenceName === 'files.autoSave' && e.newValue !== 'off') { - this.shell.saveAll(); + this.preferencesService.ready.then(() => { + this.saveResourceService.autoSave = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_PREFERENCE) ?? 'off'; + this.saveResourceService.autoSaveDelay = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_DELAY_PREFERENCE) ?? 1000; + }); + this.preferencesService.onPreferenceChanged(e => { + if (e.preferenceName === EditorCommandContribution.AUTOSAVE_PREFERENCE) { + this.saveResourceService.autoSave = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_PREFERENCE) ?? 'off'; + } else if (e.preferenceName === EditorCommandContribution.AUTOSAVE_DELAY_PREFERENCE) { + this.saveResourceService.autoSaveDelay = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_DELAY_PREFERENCE) ?? 1000; } }); } @@ -293,12 +301,7 @@ export class EditorCommandContribution implements CommandContribution { return; } const current = editor.document.languageId; - const items: Array | QuickPickItemOrSeparator> = [ - { label: nls.localizeByDefault('Auto Detect'), value: 'autoDetect' }, - { type: 'separator', label: nls.localizeByDefault('languages (identifier)') }, - ... (this.languages.languages.map(language => this.toQuickPickLanguage(language, current))).sort((e, e2) => e.label.localeCompare(e2.label)) - ]; - const selectedMode = await this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select Language Mode') }); + const selectedMode = await this.codeLanguageQuickPickService.pickEditorLanguage(current); if (selectedMode && ('value' in selectedMode)) { if (selectedMode.value === 'autoDetect') { editor.detectLanguage(); @@ -379,30 +382,6 @@ export class EditorCommandContribution implements CommandContribution { } } - protected toQuickPickLanguage(value: Language, current: string): QuickPickValue { - const languageUri = this.toLanguageUri(value); - const icon = this.labelProvider.getIcon(languageUri); - const iconClasses = icon !== '' ? [icon + ' file-icon'] : undefined; - const configured = current === value.id; - return { - value, - label: value.name, - description: nls.localizeByDefault(`({0})${configured ? ' - Configured Language' : ''}`, value.id), - iconClasses - }; - } - protected toLanguageUri(language: Language): URI { - const extension = language.extensions.values().next(); - if (extension.value) { - return new URI('file:///' + extension.value); - } - const filename = language.filenames.values().next(); - if (filename.value) { - return new URI('file:///' + filename.value); - } - return new URI('file:///.txt'); - } - protected isAutoSaveOn(): boolean { const autoSave = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_PREFERENCE); return autoSave !== 'off'; diff --git a/packages/editor/src/browser/editor-contribution.ts b/packages/editor/src/browser/editor-contribution.ts index a51191531fdf4..00dca84019da8 100644 --- a/packages/editor/src/browser/editor-contribution.ts +++ b/packages/editor/src/browser/editor-contribution.ts @@ -20,7 +20,9 @@ import { injectable, inject, optional } from '@theia/core/shared/inversify'; import { StatusBarAlignment, StatusBar } from '@theia/core/lib/browser/status-bar/status-bar'; import { FrontendApplicationContribution, DiffUris, DockLayout, - QuickInputService, KeybindingRegistry, KeybindingContribution, SHELL_TABBAR_CONTEXT_SPLIT, ApplicationShell + QuickInputService, KeybindingRegistry, KeybindingContribution, SHELL_TABBAR_CONTEXT_SPLIT, ApplicationShell, + WidgetStatusBarContribution, + Widget } from '@theia/core/lib/browser'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { CommandHandler, DisposableCollection, MenuContribution, MenuModelRegistry } from '@theia/core'; @@ -33,9 +35,9 @@ import { EditorWidget } from './editor-widget'; import { EditorLanguageStatusService } from './language-status/editor-language-status-service'; @injectable() -export class EditorContribution implements FrontendApplicationContribution, CommandContribution, KeybindingContribution, MenuContribution { +export class EditorContribution implements FrontendApplicationContribution, + CommandContribution, KeybindingContribution, MenuContribution, WidgetStatusBarContribution { - @inject(StatusBar) protected readonly statusBar: StatusBar; @inject(EditorManager) protected readonly editorManager: EditorManager; @inject(EditorLanguageStatusService) protected readonly languageStatusService: EditorLanguageStatusService; @inject(ApplicationShell) protected readonly shell: ApplicationShell; @@ -48,9 +50,6 @@ export class EditorContribution implements FrontendApplicationContribution, Comm onStart(): void { this.initEditorContextKeys(); - - this.updateStatusBar(); - this.editorManager.onCurrentEditorChanged(() => this.updateStatusBar()); } protected initEditorContextKeys(): void { @@ -72,33 +71,41 @@ export class EditorContribution implements FrontendApplicationContribution, Comm } protected readonly toDisposeOnCurrentEditorChanged = new DisposableCollection(); - protected updateStatusBar(): void { + + canHandle(widget: Widget): widget is EditorWidget { + return widget instanceof EditorWidget; + } + + activate(statusBar: StatusBar, widget: EditorWidget): void { this.toDisposeOnCurrentEditorChanged.dispose(); + const editor = widget.editor; + this.updateLanguageStatus(statusBar, editor); + this.updateEncodingStatus(statusBar, editor); + this.setCursorPositionStatus(statusBar, editor); + this.toDisposeOnCurrentEditorChanged.pushAll([ + editor.onLanguageChanged(() => this.updateLanguageStatus(statusBar, editor)), + editor.onEncodingChanged(() => this.updateEncodingStatus(statusBar, editor)), + editor.onCursorPositionChanged(() => this.setCursorPositionStatus(statusBar, editor)) + ]); + } - const widget = this.editorManager.currentEditor; - const editor = widget && widget.editor; - this.updateLanguageStatus(editor); - this.updateEncodingStatus(editor); - this.setCursorPositionStatus(editor); - if (editor) { - this.toDisposeOnCurrentEditorChanged.pushAll([ - editor.onLanguageChanged(() => this.updateLanguageStatus(editor)), - editor.onEncodingChanged(() => this.updateEncodingStatus(editor)), - editor.onCursorPositionChanged(() => this.setCursorPositionStatus(editor)) - ]); - } + deactivate(statusBar: StatusBar): void { + this.toDisposeOnCurrentEditorChanged.dispose(); + this.updateLanguageStatus(statusBar, undefined); + this.updateEncodingStatus(statusBar, undefined); + this.setCursorPositionStatus(statusBar, undefined); } - protected updateLanguageStatus(editor: TextEditor | undefined): void { + protected updateLanguageStatus(statusBar: StatusBar, editor: TextEditor | undefined): void { this.languageStatusService.updateLanguageStatus(editor); } - protected updateEncodingStatus(editor: TextEditor | undefined): void { + protected updateEncodingStatus(statusBar: StatusBar, editor: TextEditor | undefined): void { if (!editor) { - this.statusBar.removeElement('editor-status-encoding'); + statusBar.removeElement('editor-status-encoding'); return; } - this.statusBar.setElement('editor-status-encoding', { + statusBar.setElement('editor-status-encoding', { text: SUPPORTED_ENCODINGS[editor.getEncoding()].labelShort, alignment: StatusBarAlignment.RIGHT, priority: 10, @@ -107,13 +114,13 @@ export class EditorContribution implements FrontendApplicationContribution, Comm }); } - protected setCursorPositionStatus(editor: TextEditor | undefined): void { + protected setCursorPositionStatus(statusBar: StatusBar, editor: TextEditor | undefined): void { if (!editor) { - this.statusBar.removeElement('editor-status-cursor-position'); + statusBar.removeElement('editor-status-cursor-position'); return; } const { cursor } = editor; - this.statusBar.setElement('editor-status-cursor-position', { + statusBar.setElement('editor-status-cursor-position', { text: nls.localizeByDefault('Ln {0}, Col {1}', cursor.line + 1, editor.getVisibleColumn(cursor)), alignment: StatusBarAlignment.RIGHT, priority: 100, diff --git a/packages/editor/src/browser/editor-frontend-module.ts b/packages/editor/src/browser/editor-frontend-module.ts index a22365ea28e3c..d5142fa29fa9e 100644 --- a/packages/editor/src/browser/editor-frontend-module.ts +++ b/packages/editor/src/browser/editor-frontend-module.ts @@ -19,7 +19,7 @@ import '../../src/browser/language-status/editor-language-status.css'; import { ContainerModule } from '@theia/core/shared/inversify'; import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; -import { OpenHandler, WidgetFactory, FrontendApplicationContribution, KeybindingContribution } from '@theia/core/lib/browser'; +import { OpenHandler, WidgetFactory, FrontendApplicationContribution, KeybindingContribution, WidgetStatusBarContribution } from '@theia/core/lib/browser'; import { VariableContribution } from '@theia/variable-resolver/lib/browser'; import { EditorManager, EditorAccess, ActiveEditorAccess, CurrentEditorAccess } from './editor-manager'; import { EditorContribution } from './editor-contribution'; @@ -38,6 +38,7 @@ import { QuickEditorService } from './quick-editor-service'; import { EditorLanguageStatusService } from './language-status/editor-language-status-service'; import { EditorLineNumberContribution } from './editor-linenumber-contribution'; import { UndoRedoService } from './undo-redo-service'; +import { EditorLanguageQuickPickService } from './editor-language-quick-pick-service'; export default new ContainerModule(bind => { bindEditorPreferences(bind); @@ -58,7 +59,6 @@ export default new ContainerModule(bind => { bind(KeybindingContribution).toService(EditorKeybindingContribution); bind(EditorContribution).toSelf().inSingletonScope(); - bind(FrontendApplicationContribution).toService(EditorContribution); bind(EditorLanguageStatusService).toSelf().inSingletonScope(); bind(EditorLineNumberContribution).toSelf().inSingletonScope(); @@ -72,7 +72,13 @@ export default new ContainerModule(bind => { bind(VariableContribution).to(EditorVariableContribution).inSingletonScope(); - [CommandContribution, KeybindingContribution, MenuContribution].forEach(serviceIdentifier => { + [ + FrontendApplicationContribution, + WidgetStatusBarContribution, + CommandContribution, + KeybindingContribution, + MenuContribution + ].forEach(serviceIdentifier => { bind(serviceIdentifier).toService(EditorContribution); }); bind(QuickEditorService).toSelf().inSingletonScope(); @@ -84,4 +90,6 @@ export default new ContainerModule(bind => { bind(EditorAccess).to(ActiveEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.ACTIVE); bind(UndoRedoService).toSelf().inSingletonScope(); + + bind(EditorLanguageQuickPickService).toSelf().inSingletonScope(); }); diff --git a/packages/editor/src/browser/editor-generated-preference-schema.ts b/packages/editor/src/browser/editor-generated-preference-schema.ts index 7b164b19d5fcf..b75a3502b27a7 100644 --- a/packages/editor/src/browser/editor-generated-preference-schema.ts +++ b/packages/editor/src/browser/editor-generated-preference-schema.ts @@ -30,21 +30,39 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "type": "number", "default": 4, "minimum": 1, - "markdownDescription": nls.localizeByDefault('The number of spaces a tab is equal to. This setting is overridden based on the file contents when {0} is on.', '`#editor.detectIndentation#`'), + "markdownDescription": nls.localize("theia/editor/editor.tabSize", "The number of spaces a tab is equal to. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on."), + "scope": "language-overridable", + "restricted": false + }, + "editor.indentSize": { + "anyOf": [ + { + "type": "string", + "enum": [ + "tabSize" + ] + }, + { + "type": "number", + "minimum": 1 + } + ], + "default": "tabSize", + "markdownDescription": nls.localizeByDefault("The number of spaces used for indentation or `\"tabSize\"` to use the value from `#editor.tabSize#`. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on."), "scope": "language-overridable", "restricted": false }, "editor.insertSpaces": { "type": "boolean", "default": true, - "markdownDescription": nls.localizeByDefault('Insert spaces when pressing `Tab`. This setting is overridden based on the file contents when {0} is on.', `#editor.detectIndentation#`), + "markdownDescription": nls.localize("theia/editor/editor.insertSpaces", "Insert spaces when pressing `Tab`. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on."), "scope": "language-overridable", "restricted": false }, "editor.detectIndentation": { "type": "boolean", "default": true, - "markdownDescription": nls.localizeByDefault('Controls whether {0} and {1} will be automatically detected when a file is opened based on the file contents.', '`#editor.tabSize#`', '`#editor.insertSpaces#`'), + "markdownDescription": nls.localize("theia/editor/editor.detectIndentation", "Controls whether `#editor.tabSize#` and `#editor.insertSpaces#` will be automatically detected when a file is opened based on the file contents."), "scope": "language-overridable", "restricted": false }, @@ -65,7 +83,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "editor.wordBasedSuggestions": { "type": "boolean", "default": true, - "description": nls.localizeByDefault("Controls whether completions should be computed based on words in the document."), + "description": nls.localize("theia/editor/editor.wordBasedSuggestions", "Controls whether completions should be computed based on words in the document."), "scope": "language-overridable", "restricted": false }, @@ -81,7 +99,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] nls.localizeByDefault("Suggest words from all open documents of the same language."), nls.localizeByDefault("Suggest words from all open documents.") ], - "description": nls.localizeByDefault("Controls from which documents word based completions are computed."), + "description": nls.localize("theia/editor/editor.wordBasedSuggestionsMode", "Controls from which documents word based completions are computed."), "scope": "language-overridable", "restricted": false }, @@ -104,7 +122,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "editor.stablePeek": { "type": "boolean", "default": false, - "markdownDescription": nls.localizeByDefault('Keep peek editors open even when double-clicking their content or when hitting `Escape`.'), + "markdownDescription": nls.localizeByDefault("Keep peek editors open even when double-clicking their content or when hitting `Escape`."), "scope": "language-overridable", "restricted": false }, @@ -115,6 +133,33 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.experimental.asyncTokenization": { + "type": "boolean", + "default": false, + "description": nls.localizeByDefault("Controls whether the tokenization should happen asynchronously on a web worker."), + "tags": [ + "experimental" + ], + "scope": "language-overridable", + "restricted": false + }, + "editor.experimental.asyncTokenizationLogging": { + "type": "boolean", + "default": false, + "description": nls.localizeByDefault("Controls whether async tokenization should be logged. For debugging only."), + "scope": "language-overridable", + "restricted": false + }, + "editor.experimental.asyncTokenizationVerification": { + "type": "boolean", + "default": false, + "description": nls.localizeByDefault("Controls whether async tokenization should be verified against legacy background tokenization. Might slow down tokenization. For debugging only."), + "tags": [ + "experimental" + ], + "scope": "language-overridable", + "restricted": false + }, "editor.language.brackets": { "type": [ "array", @@ -182,6 +227,20 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "diffEditor.renderSideBySideInlineBreakpoint": { + "type": "number", + "default": 900, + "description": nls.localizeByDefault("If the diff editor width is smaller than this value, the inline view is used."), + "scope": "language-overridable", + "restricted": false + }, + "diffEditor.useInlineViewWhenSpaceIsLimited": { + "type": "boolean", + "default": true, + "description": nls.localizeByDefault("If enabled and the editor width is too small, the inline view is used."), + "scope": "language-overridable", + "restricted": false + }, "diffEditor.renderMarginRevertIcon": { "type": "boolean", "default": true, @@ -221,7 +280,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "markdownEnumDescriptions": [ nls.localizeByDefault("Lines will never wrap."), nls.localizeByDefault("Lines will wrap at the viewport width."), - nls.localizeByDefault('Lines will wrap according to the {0} setting.', '`#editor.wordWrap#`') + nls.localize("theia/editor/diffEditor.wordWrap2", "Lines will wrap according to the `#editor.wordWrap#` setting.") ], "scope": "language-overridable", "restricted": false @@ -229,19 +288,67 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "diffEditor.diffAlgorithm": { "type": "string", "enum": [ - "smart", - "experimental" + "legacy", + "advanced" ], - "default": "smart", + "default": "advanced", "markdownEnumDescriptions": [ nls.localizeByDefault("Uses the legacy diffing algorithm."), nls.localizeByDefault("Uses the advanced diffing algorithm.") ], + "tags": [ + "experimental" + ], + "scope": "language-overridable", + "restricted": false + }, + "diffEditor.hideUnchangedRegions.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": nls.localizeByDefault("Controls whether the diff editor shows unchanged regions."), + "scope": "language-overridable", + "restricted": false + }, + "diffEditor.hideUnchangedRegions.revealLineCount": { + "type": "integer", + "default": 20, + "markdownDescription": nls.localizeByDefault("Controls how many lines are used for unchanged regions."), + "minimum": 1, + "scope": "language-overridable", + "restricted": false + }, + "diffEditor.hideUnchangedRegions.minimumLineCount": { + "type": "integer", + "default": 3, + "markdownDescription": nls.localizeByDefault("Controls how many lines are used as a minimum for unchanged regions."), + "minimum": 1, + "scope": "language-overridable", + "restricted": false + }, + "diffEditor.hideUnchangedRegions.contextLineCount": { + "type": "integer", + "default": 3, + "markdownDescription": nls.localizeByDefault("Controls how many lines are used as context when comparing unchanged regions."), + "minimum": 1, + "scope": "language-overridable", + "restricted": false + }, + "diffEditor.experimental.showMoves": { + "type": "boolean", + "default": false, + "markdownDescription": nls.localizeByDefault("Controls whether the diff editor should show detected code moves."), + "scope": "language-overridable", + "restricted": false + }, + "diffEditor.experimental.showEmptyDecorations": { + "type": "boolean", + "default": true, + "description": nls.localizeByDefault("Controls whether the diff editor shows empty decorations to see where characters got inserted or deleted."), "scope": "language-overridable", "restricted": false }, "editor.acceptSuggestionOnCommitCharacter": { - "markdownDescription": nls.localizeByDefault('Controls whether suggestions should be accepted on commit characters. For example, in JavaScript, the semi-colon (`;`) can be a commit character that accepts a suggestion and types that character.'), + "markdownDescription": nls.localizeByDefault("Controls whether suggestions should be accepted on commit characters. For example, in JavaScript, the semi-colon (`;`) can be a commit character that accepts a suggestion and types that character."), "type": "boolean", "default": true, "scope": "language-overridable", @@ -272,17 +379,23 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "off" ], "enumDescriptions": [ - nls.localizeByDefault('Use platform APIs to detect when a Screen Reader is attached'), - nls.localizeByDefault("Optimize for usage with a Screen Reader"), - nls.localizeByDefault("Assume a screen reader is not attached") + nls.localize("theia/editor/editor.accessibilitySupport0", "Use platform APIs to detect when a Screen Reader is attached"), + nls.localize("theia/editor/editor.accessibilitySupport1", "Optimize for usage with a Screen Reader"), + nls.localize("theia/editor/editor.accessibilitySupport2", "Assume a screen reader is not attached") ], "default": "auto", + "tags": [ + "accessibility" + ], "description": nls.localizeByDefault("Controls if the UI should run in a mode where it is optimized for screen readers."), "scope": "language-overridable", "restricted": false }, "editor.accessibilityPageSize": { "description": nls.localizeByDefault("Controls the number of lines in the editor that can be read out by a screen reader at once. When we detect a screen reader we automatically set the default to be 500. Warning: this has a performance implication for numbers larger than the default."), + "tags": [ + "accessibility" + ], "type": "integer", "default": 10, "minimum": 1, @@ -309,6 +422,35 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.autoClosingComments": { + "enumDescriptions": [ + "", + nls.localizeByDefault("Use language configurations to determine when to autoclose comments."), + nls.localizeByDefault("Autoclose comments only when the cursor is to the left of whitespace."), + "" + ], + "description": nls.localizeByDefault("Controls whether the editor should automatically close comments after the user adds an opening comment."), + "type": "string", + "enum": [ + "always", + "languageDefined", + "beforeWhitespace", + "never" + ], + "default": "languageDefined", + "scope": "language-overridable", + "restricted": false + }, + "editor.screenReaderAnnounceInlineSuggestion": { + "description": nls.localizeByDefault("Control whether inline suggestions are announced by a screen reader."), + "tags": [ + "accessibility" + ], + "type": "boolean", + "default": true, + "scope": "language-overridable", + "restricted": false + }, "editor.autoClosingDelete": { "enumDescriptions": [ "", @@ -405,7 +547,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "editor.bracketPairColorization.enabled": { "type": "boolean", "default": true, - "markdownDescription": nls.localizeByDefault('Controls whether bracket pair colorization is enabled or not. Use {0} to override the bracket highlight colors.', '`#workbench.colorCustomizations#`'), + "markdownDescription": nls.localize("theia/editor/editor.bracketPairColorization.enabled", "Controls whether bracket pair colorization is enabled or not. Use `#workbench.colorCustomizations#` to override the bracket highlight colors."), "scope": "language-overridable", "restricted": false }, @@ -509,7 +651,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "default": 0, "minimum": 0, "maximum": 100, - "markdownDescription": nls.localizeByDefault('Controls the font size in pixels for CodeLens. When set to 0, 90% of `#editor.fontSize#` is used.'), + "markdownDescription": nls.localizeByDefault("Controls the font size in pixels for CodeLens. When set to 0, 90% of `#editor.fontSize#` is used."), "scope": "language-overridable", "restricted": false }, @@ -520,6 +662,15 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.colorDecoratorsLimit": { + "markdownDescription": nls.localizeByDefault("Controls the max number of color decorators that can be rendered in an editor at once."), + "type": "integer", + "default": 500, + "minimum": 1, + "maximum": 1000000, + "scope": "language-overridable", + "restricted": false + }, "editor.columnSelection": { "description": nls.localizeByDefault("Enable that the selection with the mouse and keys is doing column selection."), "type": "boolean", @@ -563,9 +714,19 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "restricted": false }, "editor.cursorSmoothCaretAnimation": { + "enumDescriptions": [ + nls.localizeByDefault("Smooth caret animation is disabled."), + nls.localizeByDefault("Smooth caret animation is enabled only when the user moves the cursor with an explicit gesture."), + nls.localizeByDefault("Smooth caret animation is always enabled.") + ], "description": nls.localizeByDefault("Controls whether the smooth caret animation should be enabled."), - "type": "boolean", - "default": false, + "type": "string", + "enum": [ + "off", + "explicit", + "on" + ], + "default": "off", "scope": "language-overridable", "restricted": false }, @@ -585,7 +746,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "restricted": false }, "editor.cursorSurroundingLines": { - "description": nls.localizeByDefault('Controls the minimal number of visible leading lines (minimum 0) and trailing lines (minimum 1) surrounding the cursor. Known as \'scrollOff\' or \'scrollOffset\' in some other editors.'), + "description": nls.localizeByDefault("Controls the minimal number of visible leading lines (minimum 0) and trailing lines (minimum 1) surrounding the cursor. Known as 'scrollOff' or 'scrollOffset' in some other editors."), "type": "integer", "default": 0, "minimum": 0, @@ -598,7 +759,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] nls.localizeByDefault("`cursorSurroundingLines` is enforced only when triggered via the keyboard or API."), nls.localizeByDefault("`cursorSurroundingLines` is enforced always.") ], - "description": nls.localizeByDefault("Controls when `#cursorSurroundingLines#` should be enforced."), + "markdownDescription": nls.localize("theia/editor/editor.cursorSurroundingLinesStyle", "Controls when `#cursorSurroundingLines#` should be enforced."), "type": "string", "enum": [ "default", @@ -627,7 +788,22 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "editor.dropIntoEditor.enabled": { "type": "boolean", "default": true, - "markdownDescription": nls.localizeByDefault("Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor)."), + "markdownDescription": nls.localize("theia/editor/editor.dropIntoEditor.enabled", "Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor)."), + "scope": "language-overridable", + "restricted": false + }, + "editor.dropIntoEditor.showDropSelector": { + "type": "string", + "markdownDescription": nls.localizeByDefault("Controls if a widget is shown when dropping files into the editor. This widget lets you control how the file is dropped."), + "enum": [ + "afterDrop", + "never" + ], + "enumDescriptions": [ + nls.localizeByDefault("Show the drop selector widget after a file is dropped into the editor."), + nls.localizeByDefault("Never show the drop selector widget. Instead the default drop provider is always used.") + ], + "default": "afterDrop", "scope": "language-overridable", "restricted": false }, @@ -638,6 +814,23 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.experimentalWhitespaceRendering": { + "enumDescriptions": [ + nls.localizeByDefault("Use a new rendering method with svgs."), + nls.localizeByDefault("Use a new rendering method with font characters."), + nls.localizeByDefault("Use the stable rendering method.") + ], + "description": nls.localizeByDefault("Controls whether whitespace is rendered with a new, experimental method."), + "type": "string", + "enum": [ + "svg", + "font", + "off" + ], + "default": "svg", + "scope": "language-overridable", + "restricted": false + }, "editor.fastScrollSensitivity": { "markdownDescription": nls.localizeByDefault("Scrolling speed multiplier when pressing `Alt`."), "type": "number", @@ -678,11 +871,11 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] ], "default": "never", "enumDescriptions": [ - nls.localizeByDefault('Never turn on Find in Selection automatically (default).'), - nls.localizeByDefault('Always turn on Find in Selection automatically.'), - nls.localizeByDefault('Turn on Find in Selection automatically when multiple lines of content are selected.'), + nls.localizeByDefault("Never turn on Find in Selection automatically (default)."), + nls.localizeByDefault("Always turn on Find in Selection automatically."), + nls.localizeByDefault("Turn on Find in Selection automatically when multiple lines of content are selected.") ], - "description": nls.localizeByDefault('Controls the condition for turning on Find in Selection automatically.'), + "description": nls.localizeByDefault("Controls the condition for turning on Find in Selection automatically."), "scope": "language-overridable", "restricted": false }, @@ -817,6 +1010,22 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.fontVariations": { + "anyOf": [ + { + "type": "boolean", + "description": nls.localizeByDefault("Enables/Disables the translation from font-weight to font-variation-settings. Change this to a string for fine-grained control of the 'font-variation-settings' CSS property.") + }, + { + "type": "string", + "description": nls.localizeByDefault("Explicit 'font-variation-settings' CSS property. A boolean can be passed instead if one only needs to translate font-weight to font-variation-settings.") + } + ], + "description": nls.localizeByDefault("Configures font variations. Can be either a boolean to enable/disable the translation from font-weight to font-variation-settings or a string for the value of the CSS 'font-variation-settings' property."), + "default": false, + "scope": "language-overridable", + "restricted": false + }, "editor.formatOnPaste": { "description": nls.localizeByDefault("Controls whether the editor should automatically format the pasted content. A formatter must be available and the formatter should be able to format a range in a document."), "type": "boolean", @@ -856,7 +1065,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "enumDescriptions": [ nls.localizeByDefault("Show Peek view of the results (default)"), nls.localizeByDefault("Go to the primary result and show a Peek view"), - nls.localizeByDefault('Go to the primary result and enable Peek-less navigation to others') + nls.localizeByDefault("Go to the primary result and enable Peek-less navigation to others") ], "scope": "language-overridable", "restricted": false @@ -873,7 +1082,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "enumDescriptions": [ nls.localizeByDefault("Show Peek view of the results (default)"), nls.localizeByDefault("Go to the primary result and show a Peek view"), - nls.localizeByDefault('Go to the primary result and enable Peek-less navigation to others') + nls.localizeByDefault("Go to the primary result and enable Peek-less navigation to others") ], "scope": "language-overridable", "restricted": false @@ -890,7 +1099,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "enumDescriptions": [ nls.localizeByDefault("Show Peek view of the results (default)"), nls.localizeByDefault("Go to the primary result and show a Peek view"), - nls.localizeByDefault('Go to the primary result and enable Peek-less navigation to others') + nls.localizeByDefault("Go to the primary result and enable Peek-less navigation to others") ], "scope": "language-overridable", "restricted": false @@ -907,7 +1116,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "enumDescriptions": [ nls.localizeByDefault("Show Peek view of the results (default)"), nls.localizeByDefault("Go to the primary result and show a Peek view"), - nls.localizeByDefault('Go to the primary result and enable Peek-less navigation to others') + nls.localizeByDefault("Go to the primary result and enable Peek-less navigation to others") ], "scope": "language-overridable", "restricted": false @@ -924,7 +1133,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "enumDescriptions": [ nls.localizeByDefault("Show Peek view of the results (default)"), nls.localizeByDefault("Go to the primary result and show a Peek view"), - nls.localizeByDefault('Go to the primary result and enable Peek-less navigation to others') + nls.localizeByDefault("Go to the primary result and enable Peek-less navigation to others") ], "scope": "language-overridable", "restricted": false @@ -1064,6 +1273,14 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.hover.hidingDelay": { + "type": "integer", + "minimum": 0, + "default": 300, + "description": nls.localize("theia/editor/editor.hover.hidingDelay", "Controls the delay in milliseconds after thich the hover is hidden. Requires `editor.hover.sticky` to be enabled."), + "scope": "language-overridable", + "restricted": false + }, "editor.hover.above": { "type": "boolean", "default": true, @@ -1078,6 +1295,28 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.inlineSuggest.showToolbar": { + "type": "string", + "default": "onHover", + "enum": [ + "always", + "onHover" + ], + "enumDescriptions": [ + nls.localizeByDefault("Show the inline suggestion toolbar whenever an inline suggestion is shown."), + nls.localizeByDefault("Show the inline suggestion toolbar when hovering over an inline suggestion.") + ], + "description": nls.localizeByDefault("Controls when to show the inline suggestion toolbar."), + "scope": "language-overridable", + "restricted": false + }, + "editor.inlineSuggest.suppressSuggestions": { + "type": "boolean", + "default": false, + "description": nls.localizeByDefault("Controls how inline suggestions interact with the suggest widget. If enabled, the suggest widget is not shown automatically when inline suggestions are available."), + "scope": "language-overridable", + "restricted": false + }, "editor.letterSpacing": { "description": nls.localizeByDefault("Controls the letter spacing in pixels."), "type": "number", @@ -1088,7 +1327,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "editor.lightbulb.enabled": { "type": "boolean", "default": true, - "description": nls.localizeByDefault('Enables the Code Action lightbulb in the editor.'), + "description": nls.localizeByDefault("Enables the Code Action lightbulb in the editor."), "scope": "language-overridable", "restricted": false }, @@ -1119,7 +1358,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "restricted": false }, "editor.linkedEditing": { - "description": nls.localizeByDefault('Controls whether the editor has linked editing enabled. Depending on the language, related symbols such as HTML tags, are updated while editing.'), + "description": nls.localizeByDefault("Controls whether the editor has linked editing enabled. Depending on the language, related symbols such as HTML tags, are updated while editing."), "type": "boolean", "default": false, "scope": "language-overridable", @@ -1276,8 +1515,17 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.multiCursorLimit": { + "markdownDescription": nls.localizeByDefault("Controls the max number of cursors that can be in an active editor at once."), + "type": "integer", + "default": 10000, + "minimum": 1, + "maximum": 100000, + "scope": "language-overridable", + "restricted": false + }, "editor.occurrencesHighlight": { - "description": nls.localizeByDefault("Controls whether the editor should highlight semantic symbol occurrences."), + "description": nls.localize("theia/editor/editor.occurrencesHighlight", "Controls whether the editor should highlight semantic symbol occurrences."), "type": "boolean", "default": true, "scope": "language-overridable", @@ -1308,6 +1556,28 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.pasteAs.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": nls.localizeByDefault("Controls whether you can paste content in different ways."), + "scope": "language-overridable", + "restricted": false + }, + "editor.pasteAs.showPasteSelector": { + "type": "string", + "markdownDescription": nls.localizeByDefault("Controls if a widget is shown when pasting content in to the editor. This widget lets you control how the file is pasted."), + "enum": [ + "afterPaste", + "never" + ], + "enumDescriptions": [ + nls.localizeByDefault("Show the paste selector widget after content is pasted into the editor."), + nls.localizeByDefault("Never show the paste selector widget. Instead the default pasting behavior is always used.") + ], + "default": "afterPaste", + "scope": "language-overridable", + "restricted": false + }, "editor.parameterHints.enabled": { "type": "boolean", "default": true, @@ -1317,7 +1587,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] }, "editor.parameterHints.cycle": { "type": "boolean", - "default": false, + "default": true, "description": nls.localizeByDefault("Controls whether the parameter hints menu cycles or closes when reaching the end of the list."), "scope": "language-overridable", "restricted": false @@ -1451,8 +1721,13 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] }, "editor.renderFinalNewline": { "description": nls.localizeByDefault("Render last line number when the file ends with a newline."), - "type": "boolean", - "default": true, + "type": "string", + "enum": [ + "off", + "on", + "dimmed" + ], + "default": "on", "scope": "language-overridable", "restricted": false }, @@ -1619,12 +1894,6 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, - "editor.selectionClipboard": { - "type": "boolean", - "default": true, - "description": nls.localizeByDefault("Controls whether the Linux primary clipboard should be supported."), - "included": !isOSX && !isWindows - }, "editor.selectionHighlight": { "description": nls.localizeByDefault("Controls whether the editor should highlight matches similar to the selection."), "type": "boolean", @@ -1682,6 +1951,13 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.smartSelect.selectSubwords": { + "description": nls.localizeByDefault("Whether subwords (like 'foo' in 'fooBar' or 'foo_bar') should be selected."), + "default": true, + "type": "boolean", + "scope": "language-overridable", + "restricted": false + }, "editor.smoothScrolling": { "description": nls.localizeByDefault("Controls whether the editor will scroll using an animation."), "type": "boolean", @@ -1705,6 +1981,25 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.stickyScroll.defaultModel": { + "type": "string", + "enum": [ + "outlineModel", + "foldingProviderModel", + "indentationModel" + ], + "default": "outlineModel", + "description": nls.localizeByDefault("Defines the model to use for determining which lines to stick. If the outline model does not exist, it will fall back on the folding provider model which falls back on the indentation model. This order is respected in all three cases."), + "scope": "language-overridable", + "restricted": false + }, + "editor.stickyScroll.scrollWithEditor": { + "type": "boolean", + "default": true, + "description": nls.localize("theia/editor/editor.stickyScroll.scrollWithEditor", "Enable scrolling of the sticky scroll widget with the editor's horizontal scrollbar."), + "scope": "language-overridable", + "restricted": false + }, "editor.stickyTabStops": { "description": nls.localizeByDefault("Emulate selection behavior of tab characters when using spaces for indentation. Selection will stick to tab stops."), "type": "boolean", @@ -1748,9 +2043,28 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.suggest.selectionMode": { + "type": "string", + "enum": [ + "always", + "never", + "whenTriggerCharacter", + "whenQuickSuggestion" + ], + "enumDescriptions": [ + nls.localizeByDefault("Always select a suggestion when automatically triggering IntelliSense."), + nls.localizeByDefault("Never select a suggestion when automatically triggering IntelliSense."), + nls.localizeByDefault("Select a suggestion only when triggering IntelliSense from a trigger character."), + nls.localizeByDefault("Select a suggestion only when triggering IntelliSense as you type.") + ], + "default": "always", + "markdownDescription": nls.localizeByDefault("Controls whether a suggestion is selected when the widget shows. Note that this only applies to automatically triggered suggestions (`#editor.quickSuggestions#` and `#editor.suggestOnTriggerCharacters#`) and that a suggestion is always selected when explicitly invoked, e.g via `Ctrl+Space`."), + "scope": "language-overridable", + "restricted": false + }, "editor.suggest.snippetsPreventQuickSuggestions": { "type": "boolean", - "default": true, + "default": false, "description": nls.localizeByDefault("Controls whether an active snippet prevents quick suggestions."), "scope": "language-overridable", "restricted": false @@ -1779,7 +2093,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "editor.suggest.showInlineDetails": { "type": "boolean", "default": true, - "description": nls.localizeByDefault('Controls whether suggest details show inline with the label or only in the details widget.'), + "description": nls.localizeByDefault("Controls whether suggest details show inline with the label or only in the details widget."), "scope": "language-overridable", "restricted": false }, @@ -1828,7 +2142,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "editor.suggest.matchOnWordStartOnly": { "type": "boolean", "default": true, - "markdownDescription": nls.localize("theia/editor/editor.suggest.matchOnWordStartOnly", "When enabled IntelliSense filtering requires that the first character matches on a word start, e.g `c` on `Console` or `WebContext` but _not_ on `description`. When disabled IntelliSense will show more results but still sorts them by match quality."), + "markdownDescription": nls.localizeByDefault("When enabled IntelliSense filtering requires that the first character matches on a word start. For example, `c` on `Console` or `WebContext` but _not_ on `description`. When disabled IntelliSense will show more results but still sorts them by match quality."), "scope": "language-overridable", "restricted": false }, @@ -2008,7 +2322,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "restricted": false }, "editor.suggestFontSize": { - "markdownDescription": nls.localizeByDefault('Font size for the suggest widget. When set to {0}, the value of {1} is used.', '`0`', '`#editor.fontSize#`'), + "markdownDescription": nls.localize("theia/editor/editor.suggestFontSize", "Font size for the suggest widget. When set to `0`, the value of `#editor.fontSize#` is used."), "type": "integer", "default": 0, "minimum": 0, @@ -2017,7 +2331,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "restricted": false }, "editor.suggestLineHeight": { - "markdownDescription": nls.localizeByDefault('Line height for the suggest widget. When set to {0}, the value of {1} is used. The minimum value is 8.', '`0`', '`#editor.lineHeight#`'), + "markdownDescription": nls.localize("theia/editor/editor.suggestLineHeight", "Line height for the suggest widget. When set to `0`, the value of `#editor.lineHeight#` is used. The minimum value is 8."), "type": "integer", "default": 0, "minimum": 0, @@ -2107,7 +2421,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "inUntrustedWorkspace" ], "default": "inUntrustedWorkspace", - "description": nls.localizeByDefault('Controls whether characters in comments should also be subject to Unicode highlighting.'), + "description": nls.localizeByDefault("Controls whether characters in comments should also be subject to Unicode highlighting."), "scope": "language-overridable" }, "editor.unicodeHighlight.includeStrings": { @@ -2122,7 +2436,7 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "inUntrustedWorkspace" ], "default": true, - "description": nls.localizeByDefault('Controls whether characters in strings should also be subject to Unicode highlighting.'), + "description": nls.localizeByDefault("Controls whether characters in strings should also be subject to Unicode highlighting."), "scope": "language-overridable" }, "editor.unicodeHighlight.allowedCharacters": { @@ -2166,12 +2480,27 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "restricted": false }, "editor.useTabStops": { - "description": nls.localizeByDefault("Inserting and deleting whitespace follows tab stops."), + "description": nls.localize("theia/editor/editor.useTabStops", "Inserting and deleting whitespace follows tab stops."), "type": "boolean", "default": true, "scope": "language-overridable", "restricted": false }, + "editor.wordBreak": { + "markdownEnumDescriptions": [ + nls.localizeByDefault("Use the default line break rule."), + nls.localizeByDefault("Word breaks should not be used for Chinese/Japanese/Korean (CJK) text. Non-CJK text behavior is the same as for normal.") + ], + "description": nls.localizeByDefault("Controls the word break rules used for Chinese/Japanese/Korean (CJK) text."), + "type": "string", + "enum": [ + "normal", + "keepAll" + ], + "default": "normal", + "scope": "language-overridable", + "restricted": false + }, "editor.wordSeparators": { "description": nls.localizeByDefault("Characters that will be used as word separators when doing word related navigations or operations."), "type": "string", @@ -2208,13 +2537,6 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "restricted": false }, "editor.wrappingIndent": { - "enumDescriptions": [ - nls.localizeByDefault("No indentation. Wrapped lines begin at column 1."), - nls.localizeByDefault("Wrapped lines get the same indentation as the parent."), - nls.localizeByDefault("Wrapped lines get +1 indentation toward the parent."), - nls.localizeByDefault("Wrapped lines get +2 indentation toward the parent.") - ], - "description": nls.localizeByDefault("Controls the indentation of wrapped lines."), "type": "string", "enum": [ "none", @@ -2222,6 +2544,13 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "indent", "deepIndent" ], + "enumDescriptions": [ + nls.localizeByDefault("No indentation. Wrapped lines begin at column 1."), + nls.localizeByDefault("Wrapped lines get the same indentation as the parent."), + nls.localizeByDefault("Wrapped lines get +1 indentation toward the parent."), + nls.localizeByDefault("Wrapped lines get +2 indentation toward the parent.") + ], + "description": nls.localizeByDefault("Controls the indentation of wrapped lines."), "default": "same", "scope": "language-overridable", "restricted": false @@ -2231,13 +2560,13 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] nls.localizeByDefault("Assumes that all characters are of the same width. This is a fast algorithm that works correctly for monospace fonts and certain scripts (like Latin characters) where glyphs are of equal width."), nls.localizeByDefault("Delegates wrapping points computation to the browser. This is a slow algorithm, that might cause freezes for large files, but it works correctly in all cases.") ], - "description": nls.localizeByDefault('Controls the algorithm that computes wrapping points. Note that when in accessibility mode, advanced will be used for the best experience.'), "type": "string", "enum": [ "simple", "advanced" ], "default": "simple", + "description": nls.localizeByDefault("Controls the algorithm that computes wrapping points. Note that when in accessibility mode, advanced will be used for the best experience."), "scope": "language-overridable", "restricted": false }, @@ -2270,14 +2599,14 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "editor.inlayHints.fontSize": { "type": "number", "default": 0, - "markdownDescription": nls.localizeByDefault('Controls font size of inlay hints in the editor. As default the {0} is used when the configured value is less than {1} or greater than the editor font size.', '`#editor.fontSize#`'), + "markdownDescription": nls.localize("theia/editor/editor.inlayHints.fontSize", "Controls font size of inlay hints in the editor. As default the `#editor.fontSize#` is used when the configured value is less than `5` or greater than the editor font size."), "scope": "language-overridable", "restricted": false }, "editor.inlayHints.fontFamily": { "type": "string", "default": "", - "markdownDescription": nls.localizeByDefault('Controls font family of inlay hints in the editor. When set to empty, the {0} is used.', '`#editor.fontFamily#`'), + "markdownDescription": nls.localize("theia/editor/editor.inlayHints.fontFamily", "Controls font family of inlay hints in the editor. When set to empty, the `#editor.fontFamily#` is used."), "scope": "language-overridable", "restricted": false }, @@ -2288,25 +2617,73 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "scope": "language-overridable", "restricted": false }, + "editor.tabFocusMode": { + "markdownDescription": nls.localizeByDefault("Controls whether the editor receives tabs or defers them to the workbench for navigation."), + "type": "boolean", + "default": false, + "scope": "language-overridable", + "restricted": false + }, + "editor.defaultColorDecorators": { + "markdownDescription": nls.localizeByDefault("Controls whether inline color decorations should be shown using the default document color provider"), + "type": "boolean", + "default": false, + "scope": "language-overridable", + "restricted": false + }, + "editor.colorDecoratorsActivatedOn": { + "enumDescriptions": [ + nls.localizeByDefault("Make the color picker appear both on click and hover of the color decorator"), + nls.localizeByDefault("Make the color picker appear on hover of the color decorator"), + nls.localizeByDefault("Make the color picker appear on click of the color decorator") + ], + "description": nls.localizeByDefault("Controls the condition to make a color picker appear from a color decorator"), + "type": "string", + "enum": [ + "clickAndHover", + "hover", + "click" + ], + "default": "clickAndHover", + "scope": "language-overridable", + "restricted": false + }, + "editor.inlineCompletionsAccessibilityVerbose": { + "description": nls.localizeByDefault("Controls whether the accessibility hint should be provided to screen reader users when an inline completion is shown."), + "type": "boolean", + "default": false, + "scope": "language-overridable", + "restricted": false + }, "editor.codeActionWidget.showHeaders": { "type": "boolean", - "description": nls.localize("theia/editor/editor.codeActionWidget.showHeaders", "Enable/disable showing group headers in the code action menu."), - "default": true, "scope": "language-overridable", + "description": nls.localizeByDefault("Enable/disable showing group headers in the Code Action menu."), + "default": true, "restricted": false }, - "editor.experimental.pasteActions.enabled": { + "editor.codeActionWidget.includeNearbyQuickfixes": { "type": "boolean", - "description": nls.localize('theia/editor/editor.experimental.pasteActions.enabled', "Enable/disable running edits from extensions on paste."), + "scope": "language-overridable", + "description": nls.localize("theia/editor/editor.codeActionWidget.includeNearbyQuickfixes", "Enable/disable showing nearest quickfix within a line when not currently on a diagnostic."), "default": false, + "restricted": false + }, + "editor.experimental.dropIntoEditor.defaultProvider": { + "type": "object", "scope": "language-overridable", + "description": nls.localizeByDefault("Configures the default drop provider to use for content of a given mime type."), + "default": {}, + "additionalProperties": { + "type": "string" + }, "restricted": false }, "editor.rename.enablePreview": { + "scope": "language-overridable", "description": nls.localizeByDefault("Enable/disable the ability to preview changes before renaming"), "default": true, "type": "boolean", - "scope": "language-overridable", "restricted": false }, "editor.find.globalFindClipboard": { @@ -2314,6 +2691,12 @@ export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] "default": false, "description": nls.localizeByDefault("Controls whether the Find Widget should read or modify the shared find clipboard on macOS."), "included": isOSX + }, + "editor.selectionClipboard": { + "type": "boolean", + "default": true, + "description": nls.localizeByDefault("Controls whether the Linux primary clipboard should be supported."), + "included": !isOSX && !isWindows } }; @@ -2321,6 +2704,7 @@ type QuickSuggestionValues = boolean | 'on' | 'inline' | 'off'; export interface GeneratedEditorPreferences { 'editor.tabSize': number; + 'editor.indentSize': 'tabSize' | number; 'editor.insertSpaces': boolean; 'editor.detectIndentation': boolean; 'editor.trimAutoWhitespace': boolean; @@ -2330,22 +2714,35 @@ export interface GeneratedEditorPreferences { 'editor.semanticHighlighting.enabled': true | false | 'configuredByTheme'; 'editor.stablePeek': boolean; 'editor.maxTokenizationLineLength': number; + 'editor.experimental.asyncTokenization': boolean; + 'editor.experimental.asyncTokenizationLogging': boolean; + 'editor.experimental.asyncTokenizationVerification': boolean; 'editor.language.brackets': Array<[string, string]> | null | 'null'; 'editor.language.colorizedBracketPairs': Array<[string, string]> | null; 'diffEditor.maxComputationTime': number; 'diffEditor.maxFileSize': number; 'diffEditor.renderSideBySide': boolean; + 'diffEditor.renderSideBySideInlineBreakpoint': number; + 'diffEditor.useInlineViewWhenSpaceIsLimited': boolean; 'diffEditor.renderMarginRevertIcon': boolean; 'diffEditor.ignoreTrimWhitespace': boolean; 'diffEditor.renderIndicators': boolean; 'diffEditor.codeLens': boolean; 'diffEditor.wordWrap': 'off' | 'on' | 'inherit'; - 'diffEditor.diffAlgorithm': 'smart' | 'experimental'; + 'diffEditor.diffAlgorithm': 'legacy' | 'advanced'; + 'diffEditor.hideUnchangedRegions.enabled': boolean; + 'diffEditor.hideUnchangedRegions.revealLineCount': number; + 'diffEditor.hideUnchangedRegions.minimumLineCount': number; + 'diffEditor.hideUnchangedRegions.contextLineCount': number; + 'diffEditor.experimental.showMoves': boolean; + 'diffEditor.experimental.showEmptyDecorations': boolean; 'editor.acceptSuggestionOnCommitCharacter': boolean; 'editor.acceptSuggestionOnEnter': 'on' | 'smart' | 'off'; 'editor.accessibilitySupport': 'auto' | 'on' | 'off'; 'editor.accessibilityPageSize': number; 'editor.autoClosingBrackets': 'always' | 'languageDefined' | 'beforeWhitespace' | 'never'; + 'editor.autoClosingComments': 'always' | 'languageDefined' | 'beforeWhitespace' | 'never'; + 'editor.screenReaderAnnounceInlineSuggestion': boolean; 'editor.autoClosingDelete': 'always' | 'auto' | 'never'; 'editor.autoClosingOvertype': 'always' | 'auto' | 'never'; 'editor.autoClosingQuotes': 'always' | 'languageDefined' | 'beforeWhitespace' | 'never'; @@ -2362,19 +2759,22 @@ export interface GeneratedEditorPreferences { 'editor.codeLensFontFamily': string; 'editor.codeLensFontSize': number; 'editor.colorDecorators': boolean; + 'editor.colorDecoratorsLimit': number; 'editor.columnSelection': boolean; 'editor.comments.insertSpace': boolean; 'editor.comments.ignoreEmptyLines': boolean; 'editor.copyWithSyntaxHighlighting': boolean; 'editor.cursorBlinking': 'blink' | 'smooth' | 'phase' | 'expand' | 'solid'; - 'editor.cursorSmoothCaretAnimation': boolean; + 'editor.cursorSmoothCaretAnimation': 'off' | 'explicit' | 'on'; 'editor.cursorStyle': 'line' | 'block' | 'underline' | 'line-thin' | 'block-outline' | 'underline-thin'; 'editor.cursorSurroundingLines': number; 'editor.cursorSurroundingLinesStyle': 'default' | 'all'; 'editor.cursorWidth': number; 'editor.dragAndDrop': boolean; 'editor.dropIntoEditor.enabled': boolean; + 'editor.dropIntoEditor.showDropSelector': 'afterDrop' | 'never'; 'editor.emptySelectionClipboard': boolean; + 'editor.experimentalWhitespaceRendering': 'svg' | 'font' | 'off'; 'editor.fastScrollSensitivity': number; 'editor.find.cursorMoveOnType': boolean; 'editor.find.seedSearchStringFromSelection': 'never' | 'always' | 'selection'; @@ -2391,6 +2791,7 @@ export interface GeneratedEditorPreferences { 'editor.fontLigatures': boolean | string; 'editor.fontSize': number; 'editor.fontWeight': number | string | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; + 'editor.fontVariations': boolean | string; 'editor.formatOnPaste': boolean; 'editor.formatOnType': boolean; 'editor.glyphMargin': boolean; @@ -2409,8 +2810,11 @@ export interface GeneratedEditorPreferences { 'editor.hover.enabled': boolean; 'editor.hover.delay': number; 'editor.hover.sticky': boolean; + 'editor.hover.hidingDelay': number; 'editor.hover.above': boolean; 'editor.inlineSuggest.enabled': boolean; + 'editor.inlineSuggest.showToolbar': 'always' | 'onHover'; + 'editor.inlineSuggest.suppressSuggestions': boolean; 'editor.letterSpacing': number; 'editor.lightbulb.enabled': boolean; 'editor.lineHeight': number; @@ -2431,10 +2835,13 @@ export interface GeneratedEditorPreferences { 'editor.multiCursorMergeOverlapping': boolean; 'editor.multiCursorModifier': 'ctrlCmd' | 'alt'; 'editor.multiCursorPaste': 'spread' | 'full'; + 'editor.multiCursorLimit': number; 'editor.occurrencesHighlight': boolean; 'editor.overviewRulerBorder': boolean; 'editor.padding.top': number; 'editor.padding.bottom': number; + 'editor.pasteAs.enabled': boolean; + 'editor.pasteAs.showPasteSelector': 'afterPaste' | 'never'; 'editor.parameterHints.enabled': boolean; 'editor.parameterHints.cycle': boolean; 'editor.peekWidgetDefaultFocus': 'tree' | 'editor'; @@ -2443,7 +2850,7 @@ export interface GeneratedEditorPreferences { 'editor.quickSuggestionsDelay': number; 'editor.renameOnType': boolean; 'editor.renderControlCharacters': boolean; - 'editor.renderFinalNewline': boolean; + 'editor.renderFinalNewline': 'off' | 'on' | 'dimmed'; 'editor.renderLineHighlight': 'none' | 'gutter' | 'line' | 'all'; 'editor.renderLineHighlightOnlyWhenFocus': boolean; 'editor.renderWhitespace': 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; @@ -2457,20 +2864,23 @@ export interface GeneratedEditorPreferences { 'editor.scrollBeyondLastColumn': number; 'editor.scrollBeyondLastLine': boolean; 'editor.scrollPredominantAxis': boolean; - 'editor.selectionClipboard': boolean; 'editor.selectionHighlight': boolean; 'editor.showFoldingControls': 'always' | 'never' | 'mouseover'; 'editor.showUnused': boolean; 'editor.snippetSuggestions': 'top' | 'bottom' | 'inline' | 'none'; 'editor.smartSelect.selectLeadingAndTrailingWhitespace': boolean; + 'editor.smartSelect.selectSubwords': boolean; 'editor.smoothScrolling': boolean; 'editor.stickyScroll.enabled': boolean; 'editor.stickyScroll.maxLineCount': number; + 'editor.stickyScroll.defaultModel': 'outlineModel' | 'foldingProviderModel' | 'indentationModel'; + 'editor.stickyScroll.scrollWithEditor': boolean; 'editor.stickyTabStops': boolean; 'editor.suggest.insertMode': 'insert' | 'replace'; 'editor.suggest.filterGraceful': boolean; 'editor.suggest.localityBonus': boolean; 'editor.suggest.shareSuggestSelections': boolean; + 'editor.suggest.selectionMode': 'always' | 'never' | 'whenTriggerCharacter' | 'whenQuickSuggestion'; 'editor.suggest.snippetsPreventQuickSuggestions': boolean; 'editor.suggest.showIcons': boolean; 'editor.suggest.showStatusBar': boolean; @@ -2522,6 +2932,7 @@ export interface GeneratedEditorPreferences { 'editor.unicodeHighlight.allowedLocales': Record; 'editor.unusualLineTerminators': 'auto' | 'off' | 'prompt'; 'editor.useTabStops': boolean; + 'editor.wordBreak': 'normal' | 'keepAll'; 'editor.wordSeparators': string; 'editor.wordWrap': 'off' | 'on' | 'wordWrapColumn' | 'bounded'; 'editor.wordWrapColumn': number; @@ -2532,8 +2943,14 @@ export interface GeneratedEditorPreferences { 'editor.inlayHints.fontSize': number; 'editor.inlayHints.fontFamily': string; 'editor.inlayHints.padding': boolean; + 'editor.tabFocusMode': boolean; + 'editor.defaultColorDecorators': boolean; + 'editor.colorDecoratorsActivatedOn': 'clickAndHover' | 'hover' | 'click'; + 'editor.inlineCompletionsAccessibilityVerbose': boolean; 'editor.codeActionWidget.showHeaders': boolean; - 'editor.experimental.pasteActions.enabled': boolean; + 'editor.codeActionWidget.includeNearbyQuickfixes': boolean; + 'editor.experimental.dropIntoEditor.defaultProvider': null; 'editor.rename.enablePreview': boolean; 'editor.find.globalFindClipboard': boolean; + 'editor.selectionClipboard': boolean; } diff --git a/packages/editor/src/browser/editor-language-quick-pick-service.ts b/packages/editor/src/browser/editor-language-quick-pick-service.ts new file mode 100644 index 0000000000000..081a75b6d0fed --- /dev/null +++ b/packages/editor/src/browser/editor-language-quick-pick-service.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 { inject, injectable } from '@theia/core/shared/inversify'; +import { Language, LanguageService } from '@theia/core/lib/browser/language-service'; +import { nls, QuickInputService, QuickPickItemOrSeparator, QuickPickValue, URI } from '@theia/core'; +import { LabelProvider } from '@theia/core/lib/browser'; + +@injectable() +export class EditorLanguageQuickPickService { + @inject(LanguageService) + protected readonly languages: LanguageService; + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + async pickEditorLanguage(current: string): Promise | undefined> { + const items: Array | QuickPickItemOrSeparator> = [ + { label: nls.localizeByDefault('Auto Detect'), value: 'autoDetect' }, + { type: 'separator', label: nls.localizeByDefault('languages (identifier)') }, + ... (this.languages.languages.map(language => this.toQuickPickLanguage(language, current))).sort((e, e2) => e.label.localeCompare(e2.label)) + ]; + const selectedMode = await this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select Language Mode') }); + return (selectedMode && 'value' in selectedMode) ? selectedMode : undefined; + } + + protected toQuickPickLanguage(value: Language, current: string): QuickPickValue { + const languageUri = this.toLanguageUri(value); + const icon = this.labelProvider.getIcon(languageUri); + const iconClasses = icon !== '' ? [icon + ' file-icon'] : undefined; + const configured = current === value.id; + return { + value, + label: value.name, + description: nls.localizeByDefault(`({0})${configured ? ' - Configured Language' : ''}`, value.id), + iconClasses + }; + } + + protected toLanguageUri(language: Language): URI { + const extension = language.extensions.values().next(); + if (extension.value) { + return new URI('file:///' + extension.value); + } + const filename = language.filenames.values().next(); + if (filename.value) { + return new URI('file:///' + filename.value); + } + return new URI('file:///.txt'); + } + +} diff --git a/packages/editor/src/browser/editor-linenumber-contribution.ts b/packages/editor/src/browser/editor-linenumber-contribution.ts index 8dae394c8de72..3f42f64aeb0d0 100644 --- a/packages/editor/src/browser/editor-linenumber-contribution.ts +++ b/packages/editor/src/browser/editor-linenumber-contribution.ts @@ -69,8 +69,7 @@ export class EditorLineNumberContribution implements FrontendApplicationContribu menuPath: EDITOR_LINENUMBER_CONTEXT_MENU, anchor: event.event, args, - contextKeyService, - onHide: () => contextKeyService.dispose() + contextKeyService }); }); } diff --git a/packages/editor/src/browser/editor-manager.ts b/packages/editor/src/browser/editor-manager.ts index 6b1b2bd8ad7d7..f8767a5b4a1cb 100644 --- a/packages/editor/src/browser/editor-manager.ts +++ b/packages/editor/src/browser/editor-manager.ts @@ -16,8 +16,11 @@ import { injectable, postConstruct, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; -import { RecursivePartial, Emitter, Event, MaybePromise, CommandService } from '@theia/core/lib/common'; -import { WidgetOpenerOptions, NavigatableWidgetOpenHandler, NavigatableWidgetOptions, Widget, PreferenceService, CommonCommands } from '@theia/core/lib/browser'; +import { RecursivePartial, Emitter, Event, MaybePromise, CommandService, nls } from '@theia/core/lib/common'; +import { + WidgetOpenerOptions, NavigatableWidgetOpenHandler, NavigatableWidgetOptions, Widget, PreferenceService, CommonCommands, OpenWithService, getDefaultHandler, + defaultHandlerPriority +} from '@theia/core/lib/browser'; import { EditorWidget } from './editor-widget'; import { Range, Position, Location, TextEditor } from './editor'; import { EditorWidgetFactory } from './editor-widget-factory'; @@ -38,7 +41,7 @@ export class EditorManager extends NavigatableWidgetOpenHandler { readonly id = EditorWidgetFactory.ID; - readonly label = 'Code Editor'; + readonly label = nls.localizeByDefault('Text Editor'); protected readonly editorCounters = new Map(); @@ -56,6 +59,7 @@ export class EditorManager extends NavigatableWidgetOpenHandler { @inject(CommandService) protected readonly commands: CommandService; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; + @inject(OpenWithService) protected readonly openWithService: OpenWithService; @postConstruct() protected override init(): void { @@ -84,6 +88,16 @@ export class EditorManager extends NavigatableWidgetOpenHandler { this.addRecentlyVisible(widget); } } + this.openWithService.registerHandler({ + id: 'default', + label: this.label, + providerName: nls.localizeByDefault('Built-in'), + canHandle: () => 100, + // Higher priority than any other handler + // so that the text editor always appears first in the quick pick + getOrder: () => 10000, + open: uri => this.open(uri) + }); this.updateCurrentEditor(); } @@ -102,7 +116,7 @@ export class EditorManager extends NavigatableWidgetOpenHandler { if (!(editorPromise instanceof Widget)) { editorPromise.then(editor => this.revealSelection(editor, options, uri)); } else { - this.revealSelection(editorPromise, options); + this.revealSelection(editorPromise, options, uri); } } return editorPromise; @@ -173,10 +187,8 @@ export class EditorManager extends NavigatableWidgetOpenHandler { return this._currentEditor; } protected setCurrentEditor(current: EditorWidget | undefined): void { - if (this._currentEditor !== current) { - this._currentEditor = current; - this.onCurrentEditorChangedEmitter.fire(this._currentEditor); - } + this._currentEditor = current; + this.onCurrentEditorChangedEmitter.fire(this._currentEditor); } protected updateCurrentEditor(): void { const widget = this.shell.currentWidget; @@ -188,6 +200,9 @@ export class EditorManager extends NavigatableWidgetOpenHandler { } canHandle(uri: URI, options?: WidgetOpenerOptions): number { + if (getDefaultHandler(uri, this.preferenceService) === 'default') { + return defaultHandlerPriority; + } return 100; } @@ -258,7 +273,10 @@ export class EditorManager extends NavigatableWidgetOpenHandler { editor.revealPosition(selection); } else if (Range.is(selection)) { editor.cursor = selection.end; - editor.selection = selection; + editor.selection = { + ...selection, + direction: 'ltr' + }; editor.revealRange(selection); } } diff --git a/packages/editor/src/browser/editor-variable-contribution.ts b/packages/editor/src/browser/editor-variable-contribution.ts index d732f4eeaf337..4d8a3ffcbcb7c 100644 --- a/packages/editor/src/browser/editor-variable-contribution.ts +++ b/packages/editor/src/browser/editor-variable-contribution.ts @@ -39,7 +39,15 @@ export class EditorVariableContribution implements VariableContribution { description: 'The current selected text in the active file', resolve: () => { const editor = this.getCurrentEditor(); - return editor ? editor.document.getText(editor.selection) : undefined; + return editor?.document.getText(editor.selection); + } + }); + variables.registerVariable({ + name: 'currentText', + description: 'The current text in the active file', + resolve: () => { + const editor = this.getCurrentEditor(); + return editor?.document.getText(); } }); } diff --git a/packages/editor/src/browser/editor-widget.ts b/packages/editor/src/browser/editor-widget.ts index 708dba0994997..bba055996c5a4 100644 --- a/packages/editor/src/browser/editor-widget.ts +++ b/packages/editor/src/browser/editor-widget.ts @@ -15,12 +15,12 @@ // ***************************************************************************** import { Disposable, SelectionService, Event, UNTITLED_SCHEME, DisposableCollection } from '@theia/core/lib/common'; -import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock, TabBar, DockPanel } from '@theia/core/lib/browser'; +import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock, TabBar, DockPanel, unlock, ExtractableWidget } from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { find } from '@theia/core/shared/@phosphor/algorithm'; import { TextEditor } from './editor'; -export class EditorWidget extends BaseWidget implements SaveableSource, Navigatable, StatefulWidget { +export class EditorWidget extends BaseWidget implements SaveableSource, Navigatable, StatefulWidget, ExtractableWidget { protected toDisposeOnTabbarChange = new DisposableCollection(); protected currentTabbar: TabBar | undefined; @@ -38,12 +38,21 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata this.toDispose.push(this.toDisposeOnTabbarChange); this.toDispose.push(this.editor.onSelectionChanged(() => this.setSelection())); this.toDispose.push(this.editor.onFocusChanged(() => this.setSelection())); + this.toDispose.push(this.editor.onDidChangeReadOnly(isReadonly => { + if (isReadonly) { + lock(this.title); + } else { + unlock(this.title); + } + })); this.toDispose.push(Disposable.create(() => { if (this.selectionService.selection === this.editor) { this.selectionService.selection = undefined; } })); } + isExtractable: boolean = true; + secondaryWindow: Window | undefined; setSelection(): void { if (this.editor.isFocused() && this.selectionService.selection !== this.editor) { diff --git a/packages/editor/src/browser/editor.ts b/packages/editor/src/browser/editor.ts index 8a425e91970f6..0ee8222ac59f9 100644 --- a/packages/editor/src/browser/editor.ts +++ b/packages/editor/src/browser/editor.ts @@ -20,6 +20,7 @@ import URI from '@theia/core/lib/common/uri'; import { Event, Disposable, TextDocumentContentChangeDelta, Reference, isObject } from '@theia/core/lib/common'; import { Saveable, Navigatable, Widget } from '@theia/core/lib/browser'; import { EditorDecoration } from './decorations/editor-decoration'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export { Position, Range, Location }; @@ -207,15 +208,16 @@ export interface TextEditor extends Disposable, TextEditorSelection, Navigatable readonly node: HTMLElement; readonly uri: URI; - readonly isReadonly: boolean; + readonly isReadonly: boolean | MarkdownString; + readonly onDidChangeReadOnly: Event; readonly document: TextEditorDocument; readonly onDocumentContentChanged: Event; cursor: Position; readonly onCursorPositionChanged: Event; - selection: Range; - readonly onSelectionChanged: Event; + selection: Selection; + readonly onSelectionChanged: Event; /** * The text editor should be revealed, @@ -291,6 +293,12 @@ export interface TextEditor extends Disposable, TextEditorSelection, Navigatable setEncoding(encoding: string, mode: EncodingMode): void; readonly onEncodingChanged: Event; + + shouldDisplayDirtyDiff(): boolean; +} + +export interface Selection extends Range { + direction: 'ltr' | 'rtl'; } export interface Dimension { diff --git a/packages/electron/README.md b/packages/electron/README.md index 945e4f15c23e0..7dd51c63b0d6a 100644 --- a/packages/electron/README.md +++ b/packages/electron/README.md @@ -18,7 +18,7 @@ The `@theia/electron` extension bundles all Electron-specific dependencies and c - `@theia/electron/shared/...` - `native-keymap` (from [`native-keymap@^2.2.1`](https://www.npmjs.com/package/native-keymap)) - - `electron` (from [`electron@^23.2.4`](https://www.npmjs.com/package/electron)) + - `electron` (from [`electron@^30.1.2`](https://www.npmjs.com/package/electron)) - `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store)) - `fix-path` (from [`fix-path@^3.0.0`](https://www.npmjs.com/package/fix-path)) diff --git a/packages/electron/package.json b/packages/electron/package.json index 152857d7f1838..23e63797d9c8e 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -1,6 +1,6 @@ { "name": "@theia/electron", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Electron utility package", "dependencies": { "electron-store": "^8.0.0", @@ -8,11 +8,11 @@ "native-keymap": "^2.2.1" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0", - "@theia/re-exports": "1.44.0" + "@theia/ext-scripts": "1.54.0", + "@theia/re-exports": "1.54.0" }, "peerDependencies": { - "electron": "^23.2.4" + "electron": "^30.1.2" }, "theiaReExports": { "shared": { diff --git a/packages/external-terminal/package.json b/packages/external-terminal/package.json index 8e0fcd1597c64..e15aedbc83909 100644 --- a/packages/external-terminal/package.json +++ b/packages/external-terminal/package.json @@ -1,11 +1,12 @@ { "name": "@theia/external-terminal", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - External Terminal Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/workspace": "1.44.0" + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/workspace": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -41,7 +42,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts b/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts index 44180a1fa1743..2c0b34b4d1035 100644 --- a/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts +++ b/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts @@ -18,7 +18,7 @@ import * as cp from 'child_process'; import * as fs from '@theia/core/shared/fs-extra'; import { injectable } from '@theia/core/shared/inversify'; import { OS } from '@theia/core/lib/common/os'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal'; /*--------------------------------------------------------------------------------------------- diff --git a/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts b/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts index 9dd06d706fb89..7c5e8bd43acc4 100644 --- a/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts +++ b/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts @@ -16,7 +16,7 @@ import * as cp from 'child_process'; import { injectable } from '@theia/core/shared/inversify'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal'; /*--------------------------------------------------------------------------------------------- diff --git a/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts b/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts index d169339c38e95..7e8c54a1a900e 100644 --- a/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts +++ b/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts @@ -17,7 +17,7 @@ import * as cp from 'child_process'; import * as path from 'path'; import { injectable } from '@theia/core/shared/inversify'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal'; /*--------------------------------------------------------------------------------------------- diff --git a/packages/file-search/package.json b/packages/file-search/package.json index 113f6d0444c76..79861c8efdc55 100644 --- a/packages/file-search/package.json +++ b/packages/file-search/package.json @@ -1,14 +1,15 @@ { "name": "@theia/file-search", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - File Search Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/process": "1.44.0", - "@theia/workspace": "1.44.0", - "@vscode/ripgrep": "^1.14.2" + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/process": "1.54.0", + "@theia/workspace": "1.54.0", + "@vscode/ripgrep": "^1.14.2", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -44,7 +45,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/file-search/src/node/file-search-service-impl.ts b/packages/file-search/src/node/file-search-service-impl.ts index 8169a5077b788..609be17abef81 100644 --- a/packages/file-search/src/node/file-search-service-impl.ts +++ b/packages/file-search/src/node/file-search-service-impl.ts @@ -20,7 +20,7 @@ import * as readline from 'readline'; import { rgPath } from '@vscode/ripgrep'; import { injectable, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { CancellationTokenSource, CancellationToken, ILogger, isWindows } from '@theia/core'; import { RawProcessFactory } from '@theia/process/lib/node'; import { FileSearchService, WHITESPACE_QUERY_SEPARATOR } from '../common/file-search-service'; diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json index 0fcf3791a7b0e..e870dcc25eff1 100644 --- a/packages/filesystem/package.json +++ b/packages/filesystem/package.json @@ -1,24 +1,23 @@ { "name": "@theia/filesystem", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - FileSystem Extension", "dependencies": { - "@theia/core": "1.44.0", + "@theia/core": "1.54.0", "@types/body-parser": "^1.17.0", "@types/multer": "^1.4.7", - "@types/rimraf": "^2.0.2", "@types/tar-fs": "^1.16.1", - "@types/uuid": "^7.0.3", "async-mutex": "^0.3.1", "body-parser": "^1.18.3", + "browserfs": "^1.4.3", "http-status-codes": "^1.3.0", "minimatch": "^5.1.0", "multer": "1.4.4-lts.1", - "rimraf": "^2.6.2", + "rimraf": "^5.0.0", "stat-mode": "^1.0.0", "tar-fs": "^1.16.2", "trash": "^7.2.0", - "uuid": "^8.0.0", + "tslib": "^2.6.2", "vscode-languageserver-textdocument": "^1.0.1" }, "publishConfig": { @@ -33,6 +32,9 @@ "frontend": "lib/browser/filesystem-frontend-module", "backend": "lib/node/filesystem-backend-module" }, + { + "frontendOnly": "lib/browser-only/browser-only-filesystem-frontend-module" + }, { "frontend": "lib/browser/download/file-download-frontend-module", "backend": "lib/node/download/file-download-backend-module" @@ -70,7 +72,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/filesystem/src/browser-only/browser-only-filesystem-frontend-module.ts b/packages/filesystem/src/browser-only/browser-only-filesystem-frontend-module.ts new file mode 100644 index 0000000000000..eae27435b6d90 --- /dev/null +++ b/packages/filesystem/src/browser-only/browser-only-filesystem-frontend-module.ts @@ -0,0 +1,38 @@ +// ***************************************************************************** +// 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 } from '@theia/core/shared/inversify'; +import { FileSystemProvider } from '../common/files'; +import { BrowserFSFileSystemProvider } from './browserfs-filesystem-provider'; +import { RemoteFileSystemProvider, RemoteFileSystemServer } from '../common/remote-file-system-provider'; +import { BrowserFSInitialization, DefaultBrowserFSInitialization } from './browserfs-filesystem-initialization'; +import { BrowserOnlyFileSystemProviderServer } from './browser-only-filesystem-provider-server'; + +export default new ContainerModule((bind, _unbind, isBound, rebind) => { + bind(DefaultBrowserFSInitialization).toSelf(); + bind(BrowserFSFileSystemProvider).toSelf(); + bind(BrowserFSInitialization).toService(DefaultBrowserFSInitialization); + if (isBound(FileSystemProvider)) { + rebind(FileSystemProvider).to(BrowserFSFileSystemProvider).inSingletonScope(); + } else { + bind(FileSystemProvider).to(BrowserFSFileSystemProvider).inSingletonScope(); + } + if (isBound(RemoteFileSystemProvider)) { + rebind(RemoteFileSystemServer).to(BrowserOnlyFileSystemProviderServer).inSingletonScope(); + } else { + bind(RemoteFileSystemServer).to(BrowserOnlyFileSystemProviderServer).inSingletonScope(); + } +}); diff --git a/packages/filesystem/src/browser-only/browser-only-filesystem-provider-server.ts b/packages/filesystem/src/browser-only/browser-only-filesystem-provider-server.ts new file mode 100644 index 0000000000000..5b3c2b4a3a555 --- /dev/null +++ b/packages/filesystem/src/browser-only/browser-only-filesystem-provider-server.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; +import { FileSystemProviderServer } from '../common/remote-file-system-provider'; +import { Event } from '@theia/core'; + +/** + * Backend component. + * + * JSON-RPC server exposing a wrapped file system provider remotely. + */ +@injectable() +export class BrowserOnlyFileSystemProviderServer extends FileSystemProviderServer { + + // needed because users expect implicitly the RemoteFileSystemServer to be a RemoteFileSystemProxyFactory + onDidOpenConnection = Event.None; + onDidCloseConnection = Event.None; +} diff --git a/packages/filesystem/src/browser-only/browserfs-filesystem-initialization.ts b/packages/filesystem/src/browser-only/browserfs-filesystem-initialization.ts new file mode 100644 index 0000000000000..0b17d3076a468 --- /dev/null +++ b/packages/filesystem/src/browser-only/browserfs-filesystem-initialization.ts @@ -0,0 +1,61 @@ +// ***************************************************************************** +// 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 type { FSModule } from 'browserfs/dist/node/core/FS'; +import type { BrowserFSFileSystemProvider } from './browserfs-filesystem-provider'; +import { injectable } from '@theia/core/shared/inversify'; +import { FileSystem, initialize } from 'browserfs'; +import MountableFileSystem from 'browserfs/dist/node/backend/MountableFileSystem'; + +export const BrowserFSInitialization = Symbol('BrowserFSInitialization'); +export interface BrowserFSInitialization { + createMountableFileSystem(): Promise + initializeFS: (fs: FSModule, provider: BrowserFSFileSystemProvider) => Promise; +} + +@injectable() +export class DefaultBrowserFSInitialization implements BrowserFSInitialization { + + createMountableFileSystem(): Promise { + return new Promise(resolve => { + FileSystem.IndexedDB.Create({}, (e, persistedFS) => { + if (e) { + throw e; + } + if (!persistedFS) { + throw Error('Could not create filesystem'); + } + FileSystem.MountableFileSystem.Create({ + '/home': persistedFS + + }, (error, mountableFS) => { + if (error) { + throw error; + } + if (!mountableFS) { + throw Error('Could not create filesystem'); + } + initialize(mountableFS); + resolve(mountableFS); + }); + }); + }); + } + + async initializeFS(fs: FSModule, provider: BrowserFSFileSystemProvider): Promise { + + } +} diff --git a/packages/filesystem/src/browser-only/browserfs-filesystem-provider.ts b/packages/filesystem/src/browser-only/browserfs-filesystem-provider.ts new file mode 100644 index 0000000000000..ca833d9536313 --- /dev/null +++ b/packages/filesystem/src/browser-only/browserfs-filesystem-provider.ts @@ -0,0 +1,462 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/platform/files/node/diskFileSystemProvider.ts + +/* eslint-disable no-null/no-null */ + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { + FileChange, FileDeleteOptions, FileOpenOptions, + FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, + FileSystemProviderError, + FileSystemProviderErrorCode, + FileSystemProviderWithFileReadWriteCapability, + FileType, FileUpdateOptions, FileUpdateResult, FileWriteOptions, Stat, WatchOptions, createFileSystemProviderError +} from '../common/files'; +import { Event, URI, Disposable, CancellationToken } from '@theia/core'; +import { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-languageserver-protocol'; +import { ReadableStreamEvents } from '@theia/core/lib/common/stream'; +import { BFSRequire } from 'browserfs'; +import type { FSModule } from 'browserfs/dist/node/core/FS'; +import type { FileSystem } from 'browserfs/dist/node/core/file_system'; +import MountableFileSystem from 'browserfs/dist/node/backend/MountableFileSystem'; +import { basename, dirname, normalize } from 'path'; +import Stats from 'browserfs/dist/node/core/node_fs_stats'; +import { retry } from '@theia/core/lib/common/promise-util'; +import { BrowserFSInitialization } from './browserfs-filesystem-initialization'; + +// adapted from DiskFileSystemProvider +@injectable() +export class BrowserFSFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability { + capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite; + onDidChangeCapabilities: Event = Event.None; + onDidChangeFile: Event = Event.None; + onFileWatchError: Event = Event.None; + private mapHandleToPos: Map = new Map(); + private writeHandles: Set = new Set(); + private canFlush: boolean = true; + + private fs: FSModule; + private mountableFS: MountableFileSystem; + private initialized: Promise; + + constructor(@inject(BrowserFSInitialization) readonly initialization: BrowserFSInitialization) { + const init = async (): Promise => { + this.mountableFS = await initialization.createMountableFileSystem(); + this.fs = BFSRequire('fs'); + await initialization.initializeFS(this.fs, new Proxy(this, { + get(target, prop, receiver): unknown { + if (prop === 'initialized') { + return Promise.resolve(true); + } + return Reflect.get(target, prop, receiver); + } + })); + return true; + }; + this.initialized = init(); + } + + async mount(mountPoint: string, fs: FileSystem): Promise { + await this.initialized; + this.mountableFS.mount(mountPoint, fs); + }; + + watch(_resource: URI, _opts: WatchOptions): Disposable { + return Disposable.NULL; + } + async stat(resource: URI): Promise { + await this.initialized; + const path = this.toFilePath(resource); + + let stats: Stats; + try { + stats = await this.promisify(this.fs.stat)(path) as Stats; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + if (stats === undefined) { + throw new Error(`Could not read file stat for resource '${path}'`); + } + return { + type: this.toType(stats, /* symbolicLink */undefined), // FIXME: missing symbolicLink + ctime: stats.birthtime.getTime(), // intentionally not using ctime here, we want the creation time + mtime: stats.mtime.getTime(), + size: stats.size, + // FIXME: missing mode, permissions + }; + + } + async mkdir(resource: URI): Promise { + await this.initialized; + try { + await this.promisify(this.fs.mkdir)(this.toFilePath(resource)); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async readdir(resource: URI): Promise<[string, FileType][]> { + await this.initialized; + try { + + const children = await this.promisify(this.fs.readdir)(this.toFilePath(resource)) as string[]; + const result: [string, FileType][] = []; + await Promise.all(children.map(async child => { + try { + const stat = await this.stat(resource.resolve(child)); + result.push([child, stat.type]); + } catch (error) { + console.trace(error); // ignore errors for individual entries that can arise from permission denied + } + })); + + return result; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async delete(resource: URI, _opts: FileDeleteOptions): Promise { + await this.initialized; + // FIXME use options + try { + await this.promisify(this.fs.unlink)(this.toFilePath(resource)); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + await this.initialized; + const fromFilePath = this.toFilePath(from); + const toFilePath = this.toFilePath(to); + if (fromFilePath === toFilePath) { + return; // simulate node.js behaviour here and do a no-op if paths match + } + try { + // assume FS is path case sensitive - correct? + const targetExists = await this.promisify(this.fs.exists)(toFilePath); + if (targetExists) { + throw Error(`File '${toFilePath}' already exists.`); + } + if (fromFilePath === toFilePath) { + return Promise.resolve(); + } + + await this.promisify(this.fs.rename)(fromFilePath, toFilePath); + + const stat = await this.promisify(this.fs.lstat)(toFilePath) as Stats; + if (stat.isDirectory() || stat.isSymbolicLink()) { + return Promise.resolve(); // only for files + } + const fd = await this.promisify(open)(toFilePath, 'a'); + try { + await this.promisify(this.fs.futimes)(fd, stat.atime, new Date()); + } catch (error) { + // ignore + } + + this.promisify(this.fs.close)(fd); + } catch (error) { + // rewrite some typical errors that can happen especially around symlinks + // to something the user can better understand + if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') { + error = new Error(`Unable to move '${basename(fromFilePath)}' into '${basename(dirname(toFilePath))}' (${error.toString()}).`); + } + + throw this.toFileSystemProviderError(error); + } + } + async copy?(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + await this.initialized; + throw new Error('Method not implemented.'); + } + async readFile(resource: URI): Promise { + await this.initialized; + try { + const filePath = this.toFilePath(resource); + return await this.promisify(this.fs.readFile)(filePath) as Uint8Array; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + await this.initialized; + let handle: number | undefined = undefined; + try { + const filePath = this.toFilePath(resource); + + // Validate target unless { create: true, overwrite: true } + if (!opts.create || !opts.overwrite) { + const fileExists = await this.promisify(this.fs.exists)(filePath); + if (fileExists) { + if (!opts.overwrite) { + throw createFileSystemProviderError('File already exists', FileSystemProviderErrorCode.FileExists); + } + } else { + if (!opts.create) { + throw createFileSystemProviderError('File does not exist', FileSystemProviderErrorCode.FileNotFound); + } + } + } + + // Open + handle = await this.open(resource, { create: true }); + + // Write content at once + await this.write(handle, 0, content, 0, content.byteLength); + } catch (error) { + throw this.toFileSystemProviderError(error); + } finally { + if (typeof handle === 'number') { + await this.close(handle); + } + } + } + readFileStream?(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { + throw new Error('Method not implemented.'); + } + async open(resource: URI, opts: FileOpenOptions): Promise { + await this.initialized; + try { + const filePath = this.toFilePath(resource); + + let flags: string | undefined = undefined; + if (opts.create) { + // we take opts.create as a hint that the file is opened for writing + // as such we use 'w' to truncate an existing or create the + // file otherwise. we do not allow reading. + if (!flags) { + flags = 'w'; + } + } else { + // otherwise we assume the file is opened for reading + // as such we use 'r' to neither truncate, nor create + // the file. + flags = 'r'; + } + + const handle = await this.promisify(this.fs.open)(filePath, flags) as number; + + // remember this handle to track file position of the handle + // we init the position to 0 since the file descriptor was + // just created and the position was not moved so far (see + // also http://man7.org/linux/man-pages/man2/open.2.html - + // "The file offset is set to the beginning of the file.") + this.mapHandleToPos.set(handle, 0); + + // remember that this handle was used for writing + if (opts.create) { + this.writeHandles.add(handle); + } + + return handle; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async close(fd: number): Promise { + await this.initialized; + // remove this handle from map of positions + this.mapHandleToPos.delete(fd); + + // if a handle is closed that was used for writing, ensure + // to flush the contents to disk if possible. + if (this.writeHandles.delete(fd) && this.canFlush) { + try { + await this.promisify(this.fs.fdatasync)(fd); + } catch (error) { + // In some exotic setups it is well possible that node fails to sync + // In that case we disable flushing and log the error to our logger + this.canFlush = false; + console.error(error); + } + } + + await this.promisify(this.fs.close)(fd); + } + async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + await this.initialized; + const normalizedPos = this.normalizePos(fd, pos); + + let bytesRead: number | null = null; + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: { bytesRead: number, buffer: Uint8Array } | number = (await this.promisify(this.fs.read)(fd, data, offset, length, normalizedPos)) as any; + + if (typeof result === 'number') { + bytesRead = result; // node.d.ts fail + } else { + bytesRead = result.bytesRead; + } + + return bytesRead; + } catch (error) { + throw this.toFileSystemProviderError(error); + } finally { + this.updatePos(fd, normalizedPos, bytesRead); + } + } + async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + await this.initialized; + // we know at this point that the file to write to is truncated and thus empty + // if the write now fails, the file remains empty. as such we really try hard + // to ensure the write succeeds by retrying up to three times. + return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */); + + } + private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + await this.initialized; + const normalizedPos = this.normalizePos(fd, pos); + + let bytesWritten: number | null = null; + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: { bytesWritten: number, buffer: Uint8Array } | number = (await this.promisify(this.fs.write)(fd, data, offset, length, normalizedPos)) as any; + + if (typeof result === 'number') { + bytesWritten = result; // node.d.ts fail + } else { + bytesWritten = result.bytesWritten; + } + + return bytesWritten; + } catch (error) { + throw this.toFileSystemProviderError(error); + } finally { + this.updatePos(fd, normalizedPos, bytesWritten); + } + } + private normalizePos(fd: number, pos: number): number | null { + + // when calling fs.read/write we try to avoid passing in the "pos" argument and + // rather prefer to pass in "null" because this avoids an extra seek(pos) + // call that in some cases can even fail (e.g. when opening a file over FTP - + // see https://github.com/microsoft/vscode/issues/73884). + // + // as such, we compare the passed in position argument with our last known + // position for the file descriptor and use "null" if they match. + if (pos === this.mapHandleToPos.get(fd)) { + return null; + } + + return pos; + } + private updatePos(fd: number, pos: number | null, bytesLength: number | null): void { + const lastKnownPos = this.mapHandleToPos.get(fd); + if (typeof lastKnownPos === 'number') { + + // pos !== null signals that previously a position was used that is + // not null. node.js documentation explains, that in this case + // the internal file pointer is not moving and as such we do not move + // our position pointer. + // + // Docs: "If position is null, data will be read from the current file position, + // and the file position will be updated. If position is an integer, the file position + // will remain unchanged." + if (typeof pos === 'number') { + // do not modify the position + } else if (typeof bytesLength === 'number') { + this.mapHandleToPos.set(fd, lastKnownPos + bytesLength); + } else { + this.mapHandleToPos.delete(fd); + } + } + } + async access?(resource: URI, mode?: number | undefined): Promise { + await this.initialized; + throw new Error('Method not implemented.'); + } + async fsPath?(resource: URI): Promise { + await this.initialized; + throw new Error('Method not implemented.'); + } + async updateFile?(resource: URI, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise { + await this.initialized; + throw new Error('Method not implemented.'); + } + + private toFilePath(resource: URI): string { + return normalize(resource.path.toString()); + } + + private toType(entry: Stats, symbolicLink?: { dangling: boolean }): FileType { + // Signal file type by checking for file / directory, except: + // - symbolic links pointing to non-existing files are FileType.Unknown + // - files that are neither file nor directory are FileType.Unknown + let type: FileType; + if (symbolicLink?.dangling) { + type = FileType.Unknown; + } else if (entry.isFile()) { + type = FileType.File; + } else if (entry.isDirectory()) { + type = FileType.Directory; + } else { + type = FileType.Unknown; + } + + // Always signal symbolic link as file type additionally + if (symbolicLink) { + type |= FileType.SymbolicLink; + } + + return type; + } + + // FIXME typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private promisify(f: Function): (...args: any[]) => Promise { + // eslint-disable-next-line @typescript-eslint/tslint/config, @typescript-eslint/no-explicit-any + return function (...args: any[]) { + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + f(...args, (err: Error, result: T) => err ? reject(err) : resolve(result)); + }); + }; + } + + private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError { + if (error instanceof FileSystemProviderError) { + return error; // avoid double conversion + } + + let code: FileSystemProviderErrorCode; + switch (error.code) { + case 'ENOENT': + code = FileSystemProviderErrorCode.FileNotFound; + break; + case 'EISDIR': + code = FileSystemProviderErrorCode.FileIsADirectory; + break; + case 'ENOTDIR': + code = FileSystemProviderErrorCode.FileNotADirectory; + break; + case 'EEXIST': + code = FileSystemProviderErrorCode.FileExists; + break; + case 'EPERM': + case 'EACCES': + code = FileSystemProviderErrorCode.NoPermissions; + break; + default: + code = FileSystemProviderErrorCode.Unknown; + } + + return createFileSystemProviderError(error, code); + } +} diff --git a/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts b/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts index a6c9f8ce28565..afc42899c64db 100644 --- a/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts +++ b/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts @@ -16,7 +16,7 @@ import { injectable, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; -import { MaybeArray, nls } from '@theia/core/lib/common'; +import { MaybeArray, UNTITLED_SCHEME, nls } from '@theia/core/lib/common'; import { LabelProvider } from '@theia/core/lib/browser'; import { FileStat } from '../../common/files'; import { DirNode } from '../file-tree'; @@ -81,7 +81,9 @@ export class DefaultFileDialogService implements FileDialogService { } protected async getRootNode(folderToOpen?: FileStat): Promise { - const folderExists = folderToOpen && await this.fileService.exists(folderToOpen.resource); + const folderExists = folderToOpen + && folderToOpen.resource.scheme !== UNTITLED_SCHEME + && await this.fileService.exists(folderToOpen.resource); const folder = folderToOpen && folderExists ? folderToOpen : { resource: await this.rootProvider.getUserWorkingDir(), isDirectory: true diff --git a/packages/filesystem/src/browser/file-resource.spec.ts b/packages/filesystem/src/browser/file-resource.spec.ts new file mode 100644 index 0000000000000..5a02884695185 --- /dev/null +++ b/packages/filesystem/src/browser/file-resource.spec.ts @@ -0,0 +1,255 @@ +// ***************************************************************************** +// Copyright (C) 2024 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; +let disableJSDOM = enableJSDOM(); + +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +FrontendApplicationConfigProvider.set({}); + +import { Disposable, Emitter, URI } from '@theia/core'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { FileChangesEvent, FileChangeType, FileStatWithMetadata } from '../common/files'; +import { FileResource } from './file-resource'; +import { FileService } from './file-service'; + +disableJSDOM(); + +describe.only('file-resource', () => { + const sandbox = sinon.createSandbox(); + const mockEmitter = new Emitter(); + const mockOnChangeEmitter = new Emitter(); + const mockFileService = new FileService(); + + before(() => { + disableJSDOM = enableJSDOM(); + }); + + beforeEach(() => { + sandbox.restore(); + + sandbox.stub(mockFileService, 'onDidFilesChange').get(() => + mockOnChangeEmitter.event + ); + sandbox.stub(mockFileService, 'onDidRunOperation').returns(Disposable.NULL); + sandbox.stub(mockFileService, 'watch').get(() => + mockEmitter.event + ); + sandbox.stub(mockFileService, 'onDidChangeFileSystemProviderCapabilities').get(() => + mockEmitter.event + ); + sandbox.stub(mockFileService, 'onDidChangeFileSystemProviderReadOnlyMessage').get(() => + mockEmitter.event + ); + }); + + after(() => { + disableJSDOM(); + }); + + it('should save contents and not trigger change event', async () => { + const resource = new FileResource(new URI('file://test/file.txt'), + mockFileService, { readOnly: false, shouldOpenAsText: () => Promise.resolve(true), shouldOverwrite: () => Promise.resolve(true) }); + + const onChangeSpy = sandbox.spy(); + resource.onDidChangeContents(onChangeSpy); + + const deferred = new Deferred(); + + sandbox.stub(mockFileService, 'write') + .callsFake(() => + deferred.promise + ); + + sandbox.stub(mockFileService, 'resolve') + .resolves({ + mtime: 1, + ctime: 0, + size: 0, + etag: '', + isFile: true, + isDirectory: false, + isSymbolicLink: false, + isReadonly: false, + name: 'file.txt', + resource: new URI('file://test/file.txt') + }); + + resource.saveContents!('test'); + + await new Promise(resolve => setTimeout(resolve, 0)); + + mockOnChangeEmitter.fire(new FileChangesEvent( + [{ + resource: new URI('file://test/file.txt'), + type: FileChangeType.UPDATED + }] + )); + + await new Promise(resolve => setImmediate(resolve)); + + expect(onChangeSpy.called).to.be.false; + + deferred.resolve({ + mtime: 0, + ctime: 0, + size: 0, + etag: '', + encoding: 'utf-8', + isFile: true, + isDirectory: false, + isSymbolicLink: false, + isReadonly: false, + name: 'file.txt', + resource: new URI('file://test/file.txt') + }); + + await new Promise(resolve => setImmediate(resolve)); + + expect(resource.version).to.deep.equal({ etag: '', mtime: 0, encoding: 'utf-8' }); + }); + + it('should save content changes and not trigger change event', async () => { + sandbox.stub(mockFileService, 'hasCapability').returns(true); + + const resource = new FileResource(new URI('file://test/file.txt'), + mockFileService, { readOnly: false, shouldOpenAsText: () => Promise.resolve(true), shouldOverwrite: () => Promise.resolve(true) }); + + const onChangeSpy = sandbox.spy(); + resource.onDidChangeContents(onChangeSpy); + + sandbox.stub(mockFileService, 'read') + .resolves({ + mtime: 1, + ctime: 0, + size: 0, + etag: '', + name: 'file.txt', + resource: new URI('file://test/file.txt'), + value: 'test', + encoding: 'utf-8' + }); + + await resource.readContents!(); + + const deferred = new Deferred(); + + sandbox.stub(mockFileService, 'update') + .callsFake(() => + deferred.promise + ); + + sandbox.stub(mockFileService, 'resolve') + .resolves({ + mtime: 1, + ctime: 0, + size: 0, + etag: '', + isFile: true, + isDirectory: false, + isSymbolicLink: false, + isReadonly: false, + name: 'file.txt', + resource: new URI('file://test/file.txt') + }); + + resource.saveContentChanges!([{ + range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, + rangeLength: 0, + text: 'test' + }]); + + await new Promise(resolve => setTimeout(resolve, 0)); + + mockOnChangeEmitter.fire(new FileChangesEvent( + [{ + resource: new URI('file://test/file.txt'), + type: FileChangeType.UPDATED + }] + )); + + await new Promise(resolve => setImmediate(resolve)); + + expect(onChangeSpy.called).to.be.false; + + deferred.resolve({ + mtime: 0, + ctime: 0, + size: 0, + etag: '', + encoding: 'utf-8', + isFile: true, + isDirectory: false, + isSymbolicLink: false, + isReadonly: false, + name: 'file.txt', + resource: new URI('file://test/file.txt') + }); + + await new Promise(resolve => setImmediate(resolve)); + + expect(resource.version).to.deep.equal({ etag: '', mtime: 0, encoding: 'utf-8' }); + }); + + it('should trigger change event if file is updated and not in sync', async () => { + const resource = new FileResource(new URI('file://test/file.txt'), + mockFileService, { readOnly: false, shouldOpenAsText: () => Promise.resolve(true), shouldOverwrite: () => Promise.resolve(true) }); + + const onChangeSpy = sandbox.spy(); + resource.onDidChangeContents(onChangeSpy); + + sandbox.stub(mockFileService, 'read') + .resolves({ + mtime: 1, + ctime: 0, + size: 0, + etag: '', + name: 'file.txt', + resource: new URI('file://test/file.txt'), + value: 'test', + encoding: 'utf-8' + }); + + await resource.readContents!(); + + sandbox.stub(mockFileService, 'resolve') + .resolves({ + mtime: 2, + ctime: 0, + size: 0, + etag: '', + isFile: true, + isDirectory: false, + isSymbolicLink: false, + isReadonly: false, + name: 'file.txt', + resource: new URI('file://test/file.txt') + }); + + mockOnChangeEmitter.fire(new FileChangesEvent( + [{ + resource: new URI('file://test/file.txt'), + type: FileChangeType.UPDATED + }] + )); + + await new Promise(resolve => setImmediate(resolve)); + + expect(onChangeSpy.called).to.be.true; + }); +}); diff --git a/packages/filesystem/src/browser/file-resource.ts b/packages/filesystem/src/browser/file-resource.ts index ef515c4a31ff3..0d148fde3688d 100644 --- a/packages/filesystem/src/browser/file-resource.ts +++ b/packages/filesystem/src/browser/file-resource.ts @@ -27,6 +27,8 @@ import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { GENERAL_MAX_FILE_SIZE_MB } from './filesystem-preferences'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { nls } from '@theia/core'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; +import { Mutex } from 'async-mutex'; export interface FileResourceVersion extends ResourceVersion { readonly encoding: string; @@ -40,7 +42,7 @@ export namespace FileResourceVersion { } export interface FileResourceOptions { - isReadonly: boolean + readOnly: boolean | MarkdownString shouldOverwrite: () => Promise shouldOpenAsText: (error: string) => Promise } @@ -54,6 +56,9 @@ export class FileResource implements Resource { protected readonly onDidChangeContentsEmitter = new Emitter(); readonly onDidChangeContents: Event = this.onDidChangeContentsEmitter.event; + protected readonly onDidChangeReadOnlyEmitter = new Emitter(); + readonly onDidChangeReadOnly: Event = this.onDidChangeReadOnlyEmitter.event; + protected _version: FileResourceVersion | undefined; get version(): FileResourceVersion | undefined { return this._version; @@ -61,16 +66,19 @@ export class FileResource implements Resource { get encoding(): string | undefined { return this._version?.encoding; } - get isReadonly(): boolean { - return this.options.isReadonly || this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly); + get readOnly(): boolean | MarkdownString { + return this.options.readOnly; } + protected writingLock = new Mutex(); + constructor( readonly uri: URI, protected readonly fileService: FileService, protected readonly options: FileResourceOptions ) { this.toDispose.push(this.onDidChangeContentsEmitter); + this.toDispose.push(this.onDidChangeReadOnlyEmitter); this.toDispose.push(this.fileService.onDidFilesChange(event => { if (event.contains(this.uri)) { this.sync(); @@ -87,11 +95,30 @@ export class FileResource implements Resource { console.error(e); } this.updateSavingContentChanges(); - this.toDispose.push(this.fileService.onDidChangeFileSystemProviderCapabilities(e => { + this.toDispose.push(this.fileService.onDidChangeFileSystemProviderCapabilities(async e => { if (e.scheme === this.uri.scheme) { - this.updateSavingContentChanges(); + this.updateReadOnly(); } })); + this.toDispose.push(this.fileService.onDidChangeFileSystemProviderReadOnlyMessage(async e => { + if (e.scheme === this.uri.scheme) { + this.updateReadOnly(); + } + })); + } + + protected async updateReadOnly(): Promise { + const oldReadOnly = this.options.readOnly; + const readOnlyMessage = this.fileService.getReadOnlyMessage(this.uri); + if (readOnlyMessage) { + this.options.readOnly = readOnlyMessage; + } else { + this.options.readOnly = this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly); + } + if (this.options.readOnly !== oldReadOnly) { + this.updateSavingContentChanges(); + this.onDidChangeReadOnlyEmitter.fire(this.options.readOnly); + } } dispose(): void { @@ -192,6 +219,8 @@ export class FileResource implements Resource { const version = options?.version || this._version; const current = FileResourceVersion.is(version) ? version : undefined; const etag = current?.etag; + const releaseLock = await this.writingLock.acquire(); + try { const stat = await this.fileService.write(this.uri, content, { encoding: options?.encoding, @@ -213,6 +242,8 @@ export class FileResource implements Resource { throw ResourceError.OutOfSync({ message, stack, data: { uri: this.uri } }); } throw e; + } finally { + releaseLock(); } }; @@ -220,7 +251,7 @@ export class FileResource implements Resource { saveContents?: Resource['saveContents']; saveContentChanges?: Resource['saveContentChanges']; protected updateSavingContentChanges(): void { - if (this.isReadonly) { + if (this.readOnly) { delete this.saveContentChanges; delete this.saveContents; delete this.saveStream; @@ -239,6 +270,8 @@ export class FileResource implements Resource { throw ResourceError.NotFound({ message: 'has not been read yet', data: { uri: this.uri } }); } const etag = current?.etag; + const releaseLock = await this.writingLock.acquire(); + try { const stat = await this.fileService.update(this.uri, changes, { readEncoding: current.encoding, @@ -262,6 +295,8 @@ export class FileResource implements Resource { throw ResourceError.OutOfSync({ message, stack, data: { uri: this.uri } }); } throw e; + } finally { + releaseLock(); } }; @@ -279,6 +314,7 @@ export class FileResource implements Resource { } protected async isInSync(): Promise { try { + await this.writingLock.waitForUnlock(); const stat = await this.fileService.resolve(this.uri, { resolveMetadata: true }); return !!this.version && this.version.mtime >= stat.mtime; } catch { @@ -320,8 +356,13 @@ export class FileResourceResolver implements ResourceResolver { if (stat && stat.isDirectory) { throw new Error('The given uri is a directory: ' + this.labelProvider.getLongName(uri)); } + + const readOnlyMessage = this.fileService.getReadOnlyMessage(uri); + const isFileSystemReadOnly = this.fileService.hasCapability(uri, FileSystemProviderCapabilities.Readonly); + const readOnly = readOnlyMessage ?? (isFileSystemReadOnly ? isFileSystemReadOnly : (stat?.isReadonly ?? false)); + return new FileResource(uri, this.fileService, { - isReadonly: stat?.isReadonly ?? false, + readOnly: readOnly, shouldOverwrite: () => this.shouldOverwrite(uri), shouldOpenAsText: error => this.shouldOpenAsText(uri, error) }); diff --git a/packages/filesystem/src/browser/file-service.ts b/packages/filesystem/src/browser/file-service.ts index 5cefe4c1007b1..3945bf64787ee 100644 --- a/packages/filesystem/src/browser/file-service.ts +++ b/packages/filesystem/src/browser/file-service.ts @@ -51,7 +51,7 @@ import { toFileOperationResult, toFileSystemProviderErrorCode, ResolveFileResult, ResolveFileResultWithMetadata, MoveFileOptions, CopyFileOptions, BaseStatWithMetadata, FileDeleteOptions, FileOperationOptions, hasAccessCapability, hasUpdateCapability, - hasFileReadStreamCapability, FileSystemProviderWithFileReadStreamCapability + hasFileReadStreamCapability, FileSystemProviderWithFileReadStreamCapability, ReadOnlyMessageFileSystemProvider } from '../common/files'; import { BinaryBuffer, BinaryBufferReadable, BinaryBufferReadableStream, BinaryBufferReadableBufferedStream, BinaryBufferWriteableStream } from '@theia/core/lib/common/buffer'; import { ReadableStream, isReadableStream, isReadableBufferedStream, transform, consumeStream, peekStream, peekReadable, Readable } from '@theia/core/lib/common/stream'; @@ -68,6 +68,7 @@ import { readFileIntoStream } from '../common/io'; import { FileSystemWatcherErrorHandler } from './filesystem-watcher-error-handler'; import { FileSystemUtils } from '../common/filesystem-utils'; import { nls } from '@theia/core'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export interface FileOperationParticipant { @@ -235,6 +236,15 @@ export interface FileSystemProviderCapabilitiesChangeEvent { scheme: string; } +export interface FileSystemProviderReadOnlyMessageChangeEvent { + /** The affected file system provider for which this event was fired. */ + provider: FileSystemProvider; + /** The uri for which the provider is registered */ + scheme: string; + /** The new read only message */ + message: MarkdownString | undefined; +} + /** * Represents the `FileSystemProviderActivation` event. * This event is fired by the {@link FileService} if it wants to activate the @@ -342,6 +352,9 @@ export class FileService { private onDidChangeFileSystemProviderCapabilitiesEmitter = new Emitter(); readonly onDidChangeFileSystemProviderCapabilities = this.onDidChangeFileSystemProviderCapabilitiesEmitter.event; + private onDidChangeFileSystemProviderReadOnlyMessageEmitter = new Emitter(); + readonly onDidChangeFileSystemProviderReadOnlyMessage = this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.event; + private readonly providers = new Map(); private readonly activations = new Map>(); @@ -364,6 +377,9 @@ export class FileService { providerDisposables.push(provider.onDidChangeFile(changes => this.onDidFilesChangeEmitter.fire(new FileChangesEvent(changes)))); providerDisposables.push(provider.onFileWatchError(() => this.handleFileWatchError())); providerDisposables.push(provider.onDidChangeCapabilities(() => this.onDidChangeFileSystemProviderCapabilitiesEmitter.fire({ provider, scheme }))); + if (ReadOnlyMessageFileSystemProvider.is(provider)) { + providerDisposables.push(provider.onDidChangeReadOnlyMessage(message => this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.fire({ provider, scheme, message }))); + } return Disposable.create(() => { this.onDidChangeFileSystemProviderRegistrationsEmitter.fire({ added: false, scheme, provider }); @@ -403,6 +419,10 @@ export class FileService { return activation; } + hasProvider(scheme: string): boolean { + return this.providers.has(scheme); + } + /** * Tests if the service (i.e. any of its registered {@link FileSystemProvider}s) can handle the given resource. * @param resource `URI` of the resource to test. @@ -413,6 +433,14 @@ export class FileService { return this.providers.has(resource.scheme); } + getReadOnlyMessage(resource: URI): MarkdownString | undefined { + const provider = this.providers.get(resource.scheme); + if (ReadOnlyMessageFileSystemProvider.is(provider)) { + return provider.readOnlyMessage; + } + return undefined; + } + /** * Tests if the service (i.e the {@link FileSystemProvider} registered for the given uri scheme) provides the given capability. * @param resource `URI` of the resource to test. @@ -694,12 +722,7 @@ export class FileService { async read(resource: URI, options?: ReadTextFileOptions): Promise { const [bufferStream, decoder] = await this.doRead(resource, { ...options, - // optimization: since we know that the caller does not - // care about buffering, we indicate this to the reader. - // this reduces all the overhead the buffered reading - // has (open, read, close) if the provider supports - // unbuffered reading. - preferUnbuffered: true + preferUnbuffered: this.shouldReadUnbuffered(options) }); return { @@ -888,17 +911,25 @@ export class FileService { options.mtime < stat.mtime && options.etag !== etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size }); } + protected shouldReadUnbuffered(options?: ReadFileOptions): boolean { + // optimization: since we know that the caller does not + // care about buffering, we indicate this to the reader. + // this reduces all the overhead the buffered reading + // has (open, read, close) if the provider supports + // unbuffered reading. + // + // However, if we read only part of the file we still + // want buffered reading as otherwise we need to read + // the whole file and cut out the specified part later. + return options?.position === undefined && options?.length === undefined; + } + async readFile(resource: URI, options?: ReadFileOptions): Promise { const provider = await this.withReadProvider(resource); const stream = await this.doReadAsFileStream(provider, resource, { ...options, - // optimization: since we know that the caller does not - // care about buffering, we indicate this to the reader. - // this reduces all the overhead the buffered reading - // has (open, read, close) if the provider supports - // unbuffered reading. - preferUnbuffered: true + preferUnbuffered: this.shouldReadUnbuffered(options) }); return { diff --git a/packages/filesystem/src/browser/file-tree/file-tree-model.ts b/packages/filesystem/src/browser/file-tree/file-tree-model.ts index 05dead78f8d69..48ac295c646fc 100644 --- a/packages/filesystem/src/browser/file-tree/file-tree-model.ts +++ b/packages/filesystem/src/browser/file-tree/file-tree-model.ts @@ -21,7 +21,7 @@ import { FileStatNode, DirNode, FileNode } from './file-tree'; import { LocationService } from '../location'; import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { FileService } from '../file-service'; -import { FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, FileChange, FileOperation, FileOperationEvent } from '../../common/files'; +import { FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, FileChange } from '../../common/files'; import { MessageService } from '@theia/core/lib/common/message-service'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { FileSystemUtils } from '../../common'; @@ -45,7 +45,6 @@ export class FileTreeModel extends CompressedTreeModel implements LocationServic protected override init(): void { super.init(); this.toDispose.push(this.fileService.onDidFilesChange(changes => this.onFilesChanged(changes))); - this.toDispose.push(this.fileService.onDidRunOperation(event => this.onDidMove(event))); } get location(): URI | undefined { @@ -92,23 +91,6 @@ export class FileTreeModel extends CompressedTreeModel implements LocationServic } } - /** - * to workaround https://github.com/Axosoft/nsfw/issues/42 - */ - protected onDidMove(event: FileOperationEvent): void { - if (!event.isOperation(FileOperation.MOVE)) { - return; - } - if (event.resource.parent.toString() === event.target.resource.parent.toString()) { - // file rename - return; - } - this.refreshAffectedNodes([ - event.resource, - event.target.resource - ]); - } - protected onFilesChanged(changes: FileChangesEvent): void { if (!this.refreshAffectedNodes(this.getAffectedUris(changes)) && this.isRootAffected(changes)) { this.refresh(); diff --git a/packages/filesystem/src/browser/file-upload-service.ts b/packages/filesystem/src/browser/file-upload-service.ts index 66fdf2838dfeb..277ebb4837608 100644 --- a/packages/filesystem/src/browser/file-upload-service.ts +++ b/packages/filesystem/src/browser/file-upload-service.ts @@ -30,16 +30,15 @@ import { FileSystemPreferences } from './filesystem-preferences'; import { FileService } from './file-service'; import { ConfirmDialog, Dialog } from '@theia/core/lib/browser'; import { nls } from '@theia/core/lib/common/nls'; +import { Emitter, Event } from '@theia/core/lib/common/event'; export const HTTP_UPLOAD_URL: string = new Endpoint({ path: HTTP_FILE_UPLOAD_PATH }).getRestUrl().toString(true); -export interface CustomDataTransfer { - values(): Iterable -} +export type CustomDataTransfer = Iterable; export interface CustomDataTransferItem { - readonly id: string; asFile(): { + readonly id: string; readonly name: string; data(): Promise; } | undefined @@ -64,6 +63,12 @@ export class FileUploadService { static TARGET = 'target'; static UPLOAD = 'upload'; + protected readonly onDidUploadEmitter = new Emitter(); + + get onDidUpload(): Event { + return this.onDidUploadEmitter.event; + } + protected uploadForm: FileUploadService.Form; protected deferredUpload?: Deferred; @@ -249,9 +254,11 @@ export class FileUploadService { } catch (error) { uploadSemaphore.cancel(); if (!isCancelled(error)) { + this.messageService.error(nls.localize('theia/filesystem/uploadFailed', 'An error occurred while uploading a file. {0}', error.message)); throw error; } } + this.onDidUploadEmitter.fire(result.uploaded); return result; } @@ -342,6 +349,10 @@ export class FileUploadService { unregister(); if (xhr.status === 200) { resolve(); + } else if (xhr.status === 500 && xhr.statusText !== xhr.response) { + // internal error with cause message + // see packages/filesystem/src/node/node-file-upload-service.ts + reject(new Error(`Internal server error: ${xhr.response}`)); } else { reject(new Error(`POST request failed: ${xhr.status} ${xhr.statusText}`)); } @@ -420,10 +431,10 @@ export class FileUploadService { } protected async indexCustomDataTransfer(targetUri: URI, dataTransfer: CustomDataTransfer, context: FileUploadService.Context): Promise { - for (const item of dataTransfer.values()) { + for (const [_, item] of dataTransfer) { const fileInfo = item.asFile(); if (fileInfo) { - await this.indexFile(targetUri, new File([await fileInfo.data()], item.id), context); + await this.indexFile(targetUri, new File([await fileInfo.data()], fileInfo.id), context); } } } diff --git a/packages/filesystem/src/browser/filesystem-frontend-contribution.ts b/packages/filesystem/src/browser/filesystem-frontend-contribution.ts index 13917733ec2c5..6a39696d63e4f 100644 --- a/packages/filesystem/src/browser/filesystem-frontend-contribution.ts +++ b/packages/filesystem/src/browser/filesystem-frontend-contribution.ts @@ -14,27 +14,36 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject } from '@theia/core/shared/inversify'; -import URI from '@theia/core/lib/common/uri'; -import { environment } from '@theia/core/shared/@theia/application-package/lib/environment'; -import { MaybePromise, SelectionService, isCancelled, Emitter } from '@theia/core/lib/common'; -import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; +import { nls } from '@theia/core'; import { - FrontendApplicationContribution, ApplicationShell, - NavigatableWidget, NavigatableWidgetOptions, - Saveable, WidgetManager, StatefulWidget, FrontendApplication, ExpandableTreeNode, - CorePreferences, + ApplicationShell, CommonCommands, + CorePreferences, + ExpandableTreeNode, + FrontendApplication, + FrontendApplicationContribution, + NavigatableWidget, NavigatableWidgetOptions, + OpenerService, + Saveable, + StatefulWidget, + WidgetManager, + open } from '@theia/core/lib/browser'; import { MimeService } from '@theia/core/lib/browser/mime-service'; import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection'; -import { FileSystemPreferences } from './filesystem-preferences'; +import { Emitter, MaybePromise, SelectionService, isCancelled } from '@theia/core/lib/common'; +import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import URI from '@theia/core/lib/common/uri'; +import { environment } from '@theia/core/shared/@theia/application-package/lib/environment'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { UserWorkingDirectoryProvider } from '@theia/core/lib/browser/user-working-directory-provider'; +import { FileChangeType, FileChangesEvent, FileOperation } from '../common/files'; +import { FileDialogService, SaveFileDialogProps } from './file-dialog'; import { FileSelection } from './file-selection'; -import { FileUploadService, FileUploadResult } from './file-upload-service'; import { FileService, UserFileOperationEvent } from './file-service'; -import { FileChangesEvent, FileChangeType, FileOperation } from '../common/files'; -import { Deferred } from '@theia/core/lib/common/promise-util'; -import { nls } from '@theia/core'; +import { FileUploadResult, FileUploadService } from './file-upload-service'; +import { FileSystemPreferences } from './filesystem-preferences'; export namespace FileSystemCommands { @@ -78,6 +87,15 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri @inject(FileService) protected readonly fileService: FileService; + @inject(FileDialogService) + protected readonly fileDialogService: FileDialogService; + + @inject(OpenerService) + protected readonly openerService: OpenerService; + + @inject(UserWorkingDirectoryProvider) + protected readonly workingDirectory: UserWorkingDirectoryProvider; + protected onDidChangeEditorFileEmitter = new Emitter<{ editor: NavigatableWidget, type: FileChangeType }>(); readonly onDidChangeEditorFile = this.onDidChangeEditorFileEmitter.event; @@ -109,6 +127,9 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri await this.runEach((uri, widget) => this.applyMove(uri, widget, event)); this.resolveUserOperation(event); })())); + this.uploadService.onDidUpload(files => { + this.doHandleUpload(files); + }); } onStart?(app: FrontendApplication): MaybePromise { @@ -124,7 +145,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri commands.registerCommand(FileSystemCommands.UPLOAD, { isEnabled: (...args: unknown[]) => { const selection = this.getSelection(...args); - return !!selection && this.canUpload(selection); + return !!selection && !environment.electron.is(); }, isVisible: () => !environment.electron.is(), execute: (...args: unknown[]) => { @@ -134,16 +155,17 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri } } }); - } - - protected canUpload({ fileStat }: FileSelection): boolean { - return !environment.electron.is() && fileStat.isDirectory; + commands.registerCommand(CommonCommands.NEW_FILE, { + execute: (...args: unknown[]) => { + this.handleNewFileCommand(args); + } + }); } protected async upload(selection: FileSelection): Promise { try { const source = TreeWidgetSelection.getSource(this.selectionService.selection); - const fileUploadResult = await this.uploadService.upload(selection.fileStat.resource); + const fileUploadResult = await this.uploadService.upload(selection.fileStat.isDirectory ? selection.fileStat.resource : selection.fileStat.resource.parent); if (ExpandableTreeNode.is(selection) && source) { await source.model.expandNode(selection); } @@ -155,6 +177,42 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri } } + protected async doHandleUpload(uploads: string[]): Promise { + // Only handle single file uploads + if (uploads.length === 1) { + const uri = new URI(uploads[0]); + // Close all existing widgets for this URI + const widgets = this.shell.widgets.filter(widget => NavigatableWidget.getUri(widget)?.isEqual(uri)); + await this.shell.closeMany(widgets, { + // Don't ask to save the file if it's dirty + // The user has already confirmed the file overwrite + save: false + }); + // Open a new editor for this URI + open(this.openerService, uri); + } + } + + /** + * Opens a save dialog to create a new file. + * + * @param args The first argument is the name of the new file. The second argument is the parent directory URI. + */ + protected async handleNewFileCommand(args: unknown[]): Promise { + const fileName = (args !== undefined && typeof args[0] === 'string') ? args[0] : undefined; + const title = nls.localizeByDefault('Create File'); + const props: SaveFileDialogProps = { title, saveLabel: title, inputValue: fileName }; + + const dirUri = (args[1] instanceof URI) ? args[1] : await this.workingDirectory.getUserWorkingDir(); + const directory = await this.fileService.resolve(dirUri); + + const filePath = await this.fileDialogService.showSaveDialog(props, directory.isDirectory ? directory : undefined); + if (filePath) { + const file = await this.fileService.createFile(filePath); + open(this.openerService, file.resource); + } + } + protected getSelection(...args: unknown[]): FileSelection | undefined { const { selection } = this.selectionService; return this.toSelection(args[0]) ?? (Array.isArray(selection) ? selection.find(FileSelection.is) : this.toSelection(selection)); diff --git a/packages/filesystem/src/browser/filesystem-frontend-module.ts b/packages/filesystem/src/browser/filesystem-frontend-module.ts index c15fad6790a11..3f960edff1e77 100644 --- a/packages/filesystem/src/browser/filesystem-frontend-module.ts +++ b/packages/filesystem/src/browser/filesystem-frontend-module.ts @@ -31,8 +31,8 @@ import { RemoteFileServiceContribution } from './remote-file-service-contributio import { FileSystemWatcherErrorHandler } from './filesystem-watcher-error-handler'; import { FilepathBreadcrumbsContribution } from './breadcrumbs/filepath-breadcrumbs-contribution'; import { BreadcrumbsFileTreeWidget, createFileTreeBreadcrumbsWidget } from './breadcrumbs/filepath-breadcrumbs-container'; -import { FilesystemSaveResourceService } from './filesystem-save-resource-service'; -import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service'; +import { FilesystemSaveableService } from './filesystem-saveable-service'; +import { SaveableService } from '@theia/core/lib/browser/saveable-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bindFileSystemPreferences(bind); @@ -65,8 +65,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(FilepathBreadcrumbsContribution).toSelf().inSingletonScope(); bind(BreadcrumbsContribution).toService(FilepathBreadcrumbsContribution); - bind(FilesystemSaveResourceService).toSelf().inSingletonScope(); - rebind(SaveResourceService).toService(FilesystemSaveResourceService); + bind(FilesystemSaveableService).toSelf().inSingletonScope(); + rebind(SaveableService).toService(FilesystemSaveableService); bind(FileTreeDecoratorAdapter).toSelf().inSingletonScope(); }); diff --git a/packages/filesystem/src/browser/filesystem-preferences.ts b/packages/filesystem/src/browser/filesystem-preferences.ts index 2a363946610b1..27a35de410054 100644 --- a/packages/filesystem/src/browser/filesystem-preferences.ts +++ b/packages/filesystem/src/browser/filesystem-preferences.ts @@ -46,8 +46,7 @@ export const filesystemPreferenceSchema: PreferenceSchema = { }, default: { '**/.git/objects/**': true, - '**/.git/subtree-cache/**': true, - '**/node_modules/**': true + '**/.git/subtree-cache/**': true }, scope: 'resource' }, diff --git a/packages/filesystem/src/browser/filesystem-save-resource-service.ts b/packages/filesystem/src/browser/filesystem-saveable-service.ts similarity index 57% rename from packages/filesystem/src/browser/filesystem-save-resource-service.ts rename to packages/filesystem/src/browser/filesystem-saveable-service.ts index d35d772293296..804fad348b800 100644 --- a/packages/filesystem/src/browser/filesystem-save-resource-service.ts +++ b/packages/filesystem/src/browser/filesystem-saveable-service.ts @@ -14,31 +14,39 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { environment, nls } from '@theia/core'; +import { environment, MessageService, nls } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { Navigatable, Saveable, SaveableSource, SaveOptions, Widget, open, OpenerService, ConfirmDialog, FormatType, CommonCommands } from '@theia/core/lib/browser'; -import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service'; +import { Navigatable, Saveable, SaveableSource, SaveOptions, Widget, open, OpenerService, ConfirmDialog, CommonCommands, LabelProvider } from '@theia/core/lib/browser'; +import { SaveableService } from '@theia/core/lib/browser/saveable-service'; import URI from '@theia/core/lib/common/uri'; import { FileService } from './file-service'; import { FileDialogService } from './file-dialog'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; @injectable() -export class FilesystemSaveResourceService extends SaveResourceService { +export class FilesystemSaveableService extends SaveableService { - @inject(FileService) protected readonly fileService: FileService; - @inject(FileDialogService) protected readonly fileDialogService: FileDialogService; - @inject(OpenerService) protected readonly openerService: OpenerService; + @inject(MessageService) + protected readonly messageService: MessageService; + @inject(FileService) + protected readonly fileService: FileService; + @inject(FileDialogService) + protected readonly fileDialogService: FileDialogService; + @inject(OpenerService) + protected readonly openerService: OpenerService; + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; /** * This method ensures a few things about `widget`: * - `widget.getResourceUri()` actually returns a URI. - * - `widget.saveable.createSnapshot` is defined. + * - `widget.saveable.createSnapshot` or `widget.saveable.serialize` is defined. * - `widget.saveable.revert` is defined. */ override canSaveAs(widget: Widget | undefined): widget is Widget & SaveableSource & Navigatable { return widget !== undefined && Saveable.isSource(widget) - && typeof widget.saveable.createSnapshot === 'function' + && (typeof widget.saveable.createSnapshot === 'function' || typeof widget.saveable.serialize === 'function') && typeof widget.saveable.revert === 'function' && Navigatable.is(widget) && widget.getResourceUri() !== undefined; @@ -47,7 +55,7 @@ export class FilesystemSaveResourceService extends SaveResourceService { /** * Save `sourceWidget` to a new file picked by the user. */ - override async saveAs(sourceWidget: Widget & SaveableSource & Navigatable, options?: SaveOptions): Promise { + override async saveAs(sourceWidget: Widget & SaveableSource & Navigatable, options?: SaveOptions): Promise { let exist: boolean = false; let overwrite: boolean = false; let selected: URI | undefined; @@ -68,10 +76,11 @@ export class FilesystemSaveResourceService extends SaveResourceService { } } while ((selected && exist && !overwrite) || (selected?.isEqual(uri) && !canSave)); if (selected && selected.isEqual(uri)) { - await this.save(sourceWidget, options); + return this.save(sourceWidget, options); } else if (selected) { try { - await this.copyAndSave(sourceWidget, selected, overwrite); + await this.saveSnapshot(sourceWidget, selected, overwrite); + return selected; } catch (e) { console.warn(e); } @@ -79,30 +88,35 @@ export class FilesystemSaveResourceService extends SaveResourceService { } /** + * Saves the current snapshot of the {@link sourceWidget} to the target file + * and replaces the widget with a new one that contains the snapshot content + * * @param sourceWidget widget to save as `target`. * @param target The new URI for the widget. * @param overwrite */ - private async copyAndSave(sourceWidget: Widget & SaveableSource & Navigatable, target: URI, overwrite: boolean): Promise { - const snapshot = sourceWidget.saveable.createSnapshot!(); - if (!await this.fileService.exists(target)) { - const sourceUri = sourceWidget.getResourceUri()!; - if (this.fileService.canHandleResource(sourceUri)) { - await this.fileService.copy(sourceUri, target, { overwrite }); - } else { - await this.fileService.createFile(target); - } + protected async saveSnapshot(sourceWidget: Widget & SaveableSource & Navigatable, target: URI, overwrite: boolean): Promise { + const saveable = sourceWidget.saveable; + let buffer: BinaryBuffer; + if (saveable.serialize) { + buffer = await saveable.serialize(); + } else if (saveable.createSnapshot) { + const snapshot = saveable.createSnapshot(); + const content = Saveable.Snapshot.read(snapshot) ?? ''; + buffer = BinaryBuffer.fromString(content); + } else { + throw new Error('Cannot save the widget as the saveable does not provide a snapshot or a serialize method.'); } - const targetWidget = await open(this.openerService, target, { widgetOptions: { ref: sourceWidget } }); - const targetSaveable = Saveable.get(targetWidget); - if (targetWidget && targetSaveable && targetSaveable.applySnapshot) { - targetSaveable.applySnapshot(snapshot); - await sourceWidget.saveable.revert!(); - sourceWidget.close(); - Saveable.save(targetWidget, { formatType: FormatType.ON }); + + if (await this.fileService.exists(target)) { + // Do not fire the `onDidCreate` event as the file already exists. + await this.fileService.writeFile(target, buffer); } else { - this.messageService.error(nls.localize('theia/workspace/failApply', 'Could not apply changes to new file')); + // Ensure to actually call `create` as that fires the `onDidCreate` event. + await this.fileService.createFile(target, buffer, { overwrite }); } + await saveable.revert!(); + await open(this.openerService, target, { widgetOptions: { ref: sourceWidget, mode: 'tab-replace' } }); } async confirmOverwrite(uri: URI): Promise { @@ -113,7 +127,7 @@ export class FilesystemSaveResourceService extends SaveResourceService { // Prompt users for confirmation before overwriting. const confirmed = await new ConfirmDialog({ title: nls.localizeByDefault('Overwrite'), - msg: nls.localizeByDefault('{0} already exists. Are you sure you want to overwrite it?', uri.toString()) + msg: nls.localizeByDefault('{0} already exists. Are you sure you want to overwrite it?', this.labelProvider.getName(uri)) }).open(); return !!confirmed; } diff --git a/packages/filesystem/src/browser/location/location-renderer.tsx b/packages/filesystem/src/browser/location/location-renderer.tsx index bf89c946fb7c5..b6f61a60564b2 100644 --- a/packages/filesystem/src/browser/location/location-renderer.tsx +++ b/packages/filesystem/src/browser/location/location-renderer.tsx @@ -122,7 +122,9 @@ export class LocationListRenderer extends ReactRenderer { } override render(): void { - this.hostRoot.render(this.doRender()); + if (!this.toDispose.disposed) { + this.hostRoot.render(this.doRender()); + } } protected initResolveDirectoryCache(): void { diff --git a/packages/filesystem/src/common/files.ts b/packages/filesystem/src/common/files.ts index 865a680c0b113..15c90b8c4baae 100644 --- a/packages/filesystem/src/common/files.ts +++ b/packages/filesystem/src/common/files.ts @@ -27,6 +27,7 @@ import type { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-l import { ReadableStreamEvents } from '@theia/core/lib/common/stream'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { isObject } from '@theia/core/lib/common'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export const enum FileOperation { CREATE, @@ -524,6 +525,7 @@ export interface WatchOptions { } export const enum FileSystemProviderCapabilities { + None = 0, FileReadWrite = 1 << 1, FileOpenReadWriteClose = 1 << 2, FileReadStream = 1 << 4, @@ -765,6 +767,18 @@ export function hasUpdateCapability(provider: FileSystemProvider): provider is F return !!(provider.capabilities & FileSystemProviderCapabilities.Update); } +export interface ReadOnlyMessageFileSystemProvider { + readOnlyMessage: MarkdownString | undefined; + readonly onDidChangeReadOnlyMessage: Event; +} + +export namespace ReadOnlyMessageFileSystemProvider { + export function is(arg: unknown): arg is ReadOnlyMessageFileSystemProvider { + return isObject(arg) + && 'readOnlyMessage' in arg; + } +} + /** * Subtype of {@link FileSystemProvider} that ensures that the optional functions, needed for providers * that should be able to read & write files, are implemented. diff --git a/packages/filesystem/src/common/remote-file-system-provider.ts b/packages/filesystem/src/common/remote-file-system-provider.ts index 62a60cb6d0ab2..7035bc5460056 100644 --- a/packages/filesystem/src/common/remote-file-system-provider.ts +++ b/packages/filesystem/src/common/remote-file-system-provider.ts @@ -23,7 +23,8 @@ import { FileWriteOptions, FileOpenOptions, FileChangeType, FileSystemProviderCapabilities, FileChange, Stat, FileOverwriteOptions, WatchOptions, FileType, FileSystemProvider, FileDeleteOptions, hasOpenReadWriteCloseCapability, hasFileFolderCopyCapability, hasReadWriteCapability, hasAccessCapability, - FileSystemProviderError, FileSystemProviderErrorCode, FileUpdateOptions, hasUpdateCapability, FileUpdateResult, FileReadStreamOptions, hasFileReadStreamCapability + FileSystemProviderError, FileSystemProviderErrorCode, FileUpdateOptions, hasUpdateCapability, FileUpdateResult, FileReadStreamOptions, hasFileReadStreamCapability, + ReadOnlyMessageFileSystemProvider } from './files'; import { RpcServer, RpcProxy, RpcProxyFactory } from '@theia/core/lib/common/messaging/proxy-factory'; import { ApplicationError } from '@theia/core/lib/common/application-error'; @@ -31,6 +32,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util'; import type { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-languageserver-protocol'; import { newWriteableStream, ReadableStreamEvents } from '@theia/core/lib/common/stream'; import { CancellationToken, cancelled } from '@theia/core/lib/common/cancellation'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export const remoteFileSystemPath = '/services/remote-filesystem'; @@ -38,6 +40,7 @@ export const RemoteFileSystemServer = Symbol('RemoteFileSystemServer'); export interface RemoteFileSystemServer extends RpcServer { getCapabilities(): Promise stat(resource: string): Promise; + getReadOnlyMessage(): Promise; access(resource: string, mode?: number): Promise; fsPath(resource: string): Promise; open(resource: string, opts: FileOpenOptions): Promise; @@ -70,6 +73,7 @@ export interface RemoteFileSystemClient { notifyDidChangeFile(event: { changes: RemoteFileChange[] }): void; notifyFileWatchError(): void; notifyDidChangeCapabilities(capabilities: FileSystemProviderCapabilities): void; + notifyDidChangeReadOnlyMessage(readOnlyMessage: MarkdownString | undefined): void; onFileStreamData(handle: number, data: Uint8Array): void; onFileStreamEnd(handle: number, error: RemoteFileStreamError | undefined): void; } @@ -109,7 +113,7 @@ export class RemoteFileSystemProxyFactory extends RpcProxyFact * Wraps the remote filesystem provider living on the backend. */ @injectable() -export class RemoteFileSystemProvider implements Required, Disposable { +export class RemoteFileSystemProvider implements Required, Disposable, ReadOnlyMessageFileSystemProvider { private readonly onDidChangeFileEmitter = new Emitter(); readonly onDidChangeFile = this.onDidChangeFileEmitter.event; @@ -120,6 +124,9 @@ export class RemoteFileSystemProvider implements Required, D private readonly onDidChangeCapabilitiesEmitter = new Emitter(); readonly onDidChangeCapabilities = this.onDidChangeCapabilitiesEmitter.event; + private readonly onDidChangeReadOnlyMessageEmitter = new Emitter(); + readonly onDidChangeReadOnlyMessage = this.onDidChangeReadOnlyMessageEmitter.event; + private readonly onFileStreamDataEmitter = new Emitter<[number, Uint8Array]>(); private readonly onFileStreamData = this.onFileStreamDataEmitter.event; @@ -129,6 +136,7 @@ export class RemoteFileSystemProvider implements Required, D protected readonly toDispose = new DisposableCollection( this.onDidChangeFileEmitter, this.onDidChangeCapabilitiesEmitter, + this.onDidChangeReadOnlyMessageEmitter, this.onFileStreamDataEmitter, this.onFileStreamEndEmitter ); @@ -143,9 +151,14 @@ export class RemoteFileSystemProvider implements Required, D options: WatchOptions }>(); - private _capabilities: FileSystemProviderCapabilities = 0; + private _capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.None; get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } + private _readOnlyMessage: MarkdownString | undefined = undefined; + get readOnlyMessage(): MarkdownString | undefined { + return this._readOnlyMessage; + } + protected readonly readyDeferred = new Deferred(); readonly ready = this.readyDeferred.promise; @@ -161,6 +174,9 @@ export class RemoteFileSystemProvider implements Required, D this._capabilities = capabilities; this.readyDeferred.resolve(); }, this.readyDeferred.reject); + this.server.getReadOnlyMessage().then(readOnlyMessage => { + this._readOnlyMessage = readOnlyMessage; + }); this.server.setClient({ notifyDidChangeFile: ({ changes }) => { this.onDidChangeFileEmitter.fire(changes.map(event => ({ resource: new URI(event.resource), type: event.type }))); @@ -169,6 +185,7 @@ export class RemoteFileSystemProvider implements Required, D this.onFileWatchErrorEmitter.fire(); }, notifyDidChangeCapabilities: capabilities => this.setCapabilities(capabilities), + notifyDidChangeReadOnlyMessage: readOnlyMessage => this.setReadOnlyMessage(readOnlyMessage), onFileStreamData: (handle, data) => this.onFileStreamDataEmitter.fire([handle, data]), onFileStreamEnd: (handle, error) => this.onFileStreamEndEmitter.fire([handle, error]) }); @@ -188,6 +205,11 @@ export class RemoteFileSystemProvider implements Required, D this.onDidChangeCapabilitiesEmitter.fire(undefined); } + protected setReadOnlyMessage(readOnlyMessage: MarkdownString | undefined): void { + this._readOnlyMessage = readOnlyMessage; + this.onDidChangeReadOnlyMessageEmitter.fire(readOnlyMessage); + } + // --- forwarding calls stat(resource: URI): Promise { @@ -362,6 +384,14 @@ export class FileSystemProviderServer implements RemoteFileSystemServer { this.client.notifyDidChangeCapabilities(this.provider.capabilities); } })); + if (ReadOnlyMessageFileSystemProvider.is(this.provider)) { + const providerWithReadOnlyMessage: ReadOnlyMessageFileSystemProvider = this.provider; + this.toDispose.push(this.provider.onDidChangeReadOnlyMessage(() => { + if (this.client) { + this.client.notifyDidChangeReadOnlyMessage(providerWithReadOnlyMessage.readOnlyMessage); + } + })); + } this.toDispose.push(this.provider.onDidChangeFile(changes => { if (this.client) { this.client.notifyDidChangeFile({ @@ -380,6 +410,14 @@ export class FileSystemProviderServer implements RemoteFileSystemServer { return this.provider.capabilities; } + async getReadOnlyMessage(): Promise { + if (ReadOnlyMessageFileSystemProvider.is(this.provider)) { + return this.provider.readOnlyMessage; + } else { + return undefined; + } + } + stat(resource: string): Promise { return this.provider.stat(new URI(resource)); } diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts index c3edec5afd967..a5cd9a7a4ad63 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts @@ -28,7 +28,7 @@ import { DefaultFileDialogService, OpenFileDialogProps, SaveFileDialogProps } fr // solution. // // eslint-disable-next-line @theia/runtime-import-check -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { OpenDialogOptions, SaveDialogOptions } from '../../electron-common/electron-api'; import '@theia/core/lib/electron-common/electron-api'; diff --git a/packages/filesystem/src/node/disk-file-system-provider.spec.ts b/packages/filesystem/src/node/disk-file-system-provider.spec.ts index ed0011db27e92..a2c84055175b2 100644 --- a/packages/filesystem/src/node/disk-file-system-provider.spec.ts +++ b/packages/filesystem/src/node/disk-file-system-provider.spec.ts @@ -18,15 +18,15 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa import { EncodingService } from '@theia/core/lib/common/encoding-service'; import { ILogger } from '@theia/core/lib/common/logger'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { IPCConnectionProvider } from '@theia/core/lib/node/messaging/ipc-connection-provider'; import { Container, ContainerModule } from '@theia/core/shared/inversify'; import { equal, fail } from 'assert'; import { promises as fs } from 'fs'; import { join } from 'path'; import * as temp from 'temp'; -import { v4 } from 'uuid'; -import { FilePermission, FileSystemProviderError, FileSystemProviderErrorCode } from '../common/files'; +import { generateUuid } from '@theia/core/lib/common/uuid'; +import { FilePermission, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode } from '../common/files'; import { DiskFileSystemProvider } from './disk-file-system-provider'; import { bindFileSystemWatcherServer } from './filesystem-backend-module'; @@ -53,7 +53,7 @@ describe('disk-file-system-provider', () => { describe('stat', () => { it("should omit the 'permissions' property of the stat if the file can be both read and write", async () => { const tempDirPath = tracked.mkdirSync(); - const tempFilePath = join(tempDirPath, `${v4()}.txt`); + const tempFilePath = join(tempDirPath, `${generateUuid()}.txt`); await fs.writeFile(tempFilePath, 'some content', { encoding: 'utf8' }); let content = await fs.readFile(tempFilePath, { encoding: 'utf8' }); @@ -70,7 +70,7 @@ describe('disk-file-system-provider', () => { it("should set the 'permissions' property to `Readonly` if the file can be read but not write", async () => { const tempDirPath = tracked.mkdirSync(); - const tempFilePath = join(tempDirPath, `${v4()}.txt`); + const tempFilePath = join(tempDirPath, `${generateUuid()}.txt`); await fs.writeFile(tempFilePath, 'readonly content', { encoding: 'utf8', }); @@ -93,6 +93,39 @@ describe('disk-file-system-provider', () => { }); }); + describe('delete', () => { + it('delete is able to delete folder', async () => { + const tempDirPath = tracked.mkdirSync(); + const testFolder = join(tempDirPath, 'test'); + const folderUri = FileUri.create(testFolder); + for (const recursive of [true, false]) { + // Note: useTrash = true fails on Linux + const useTrash = false; + if ((fsProvider.capabilities & FileSystemProviderCapabilities.Access) === 0 && useTrash) { + continue; + } + await fsProvider.mkdir(folderUri); + if (recursive) { + await fsProvider.writeFile(FileUri.create(join(testFolder, 'test.file')), Buffer.from('test'), { overwrite: false, create: true }); + await fsProvider.mkdir(FileUri.create(join(testFolder, 'subFolder'))); + } + await fsProvider.delete(folderUri, { recursive, useTrash }); + } + }); + + it('delete is able to delete file', async () => { + const tempDirPath = tracked.mkdirSync(); + const testFile = join(tempDirPath, 'test.file'); + const testFileUri = FileUri.create(testFile); + for (const recursive of [true, false]) { + for (const useTrash of [true, false]) { + await fsProvider.writeFile(testFileUri, Buffer.from('test'), { overwrite: false, create: true }); + await fsProvider.delete(testFileUri, { recursive, useTrash }); + } + } + }); + }); + function createContainer(): Container { const container = new Container({ defaultScope: 'Singleton' }); const module = new ContainerModule(bind => { diff --git a/packages/filesystem/src/node/disk-file-system-provider.ts b/packages/filesystem/src/node/disk-file-system-provider.ts index b5bdf2234934c..9aad02ded5ecb 100644 --- a/packages/filesystem/src/node/disk-file-system-provider.ts +++ b/packages/filesystem/src/node/disk-file-system-provider.ts @@ -24,7 +24,7 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { basename, dirname, normalize, join } from 'path'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import * as os from 'os'; import * as fs from 'fs'; import { @@ -35,7 +35,7 @@ import { import { promisify } from 'util'; import URI from '@theia/core/lib/common/uri'; import { Path } from '@theia/core/lib/common/path'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { OS, isWindows } from '@theia/core/lib/common/os'; @@ -509,7 +509,12 @@ export class DiskFileSystemProvider implements Disposable, if (opts.recursive) { await this.rimraf(filePath); } else { - await promisify(unlink)(filePath); + const stat = await promisify(lstat)(filePath); + if (stat.isDirectory() && !stat.isSymbolicLink()) { + await promisify(rmdir)(filePath); + } else { + await promisify(unlink)(filePath); + } } } else { await trash(filePath); @@ -525,7 +530,7 @@ export class DiskFileSystemProvider implements Disposable, protected async rimrafMove(path: string): Promise { try { - const pathInTemp = join(os.tmpdir(), v4()); + const pathInTemp = join(os.tmpdir(), generateUuid()); try { await promisify(rename)(path, pathInTemp); } catch (error) { diff --git a/packages/filesystem/src/node/download/directory-archiver.spec.ts b/packages/filesystem/src/node/download/directory-archiver.spec.ts index a1c1e2c410c21..12a2752f25a8c 100644 --- a/packages/filesystem/src/node/download/directory-archiver.spec.ts +++ b/packages/filesystem/src/node/download/directory-archiver.spec.ts @@ -21,7 +21,7 @@ import { extract } from 'tar-fs'; import { expect } from 'chai'; import URI from '@theia/core/lib/common/uri'; import { MockDirectoryArchiver } from './test/mock-directory-archiver'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; const track = temp.track(); diff --git a/packages/filesystem/src/node/download/directory-archiver.ts b/packages/filesystem/src/node/download/directory-archiver.ts index c8c99526e8af8..40513d74f720e 100644 --- a/packages/filesystem/src/node/download/directory-archiver.ts +++ b/packages/filesystem/src/node/download/directory-archiver.ts @@ -18,7 +18,7 @@ import { injectable } from '@theia/core/shared/inversify'; import * as fs from '@theia/core/shared/fs-extra'; import { pack } from 'tar-fs'; import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; @injectable() export class DirectoryArchiver { diff --git a/packages/filesystem/src/node/download/file-download-cache.ts b/packages/filesystem/src/node/download/file-download-cache.ts index d1bb3285958b8..067e41458b59f 100644 --- a/packages/filesystem/src/node/download/file-download-cache.ts +++ b/packages/filesystem/src/node/download/file-download-cache.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { injectable, inject } from '@theia/core/shared/inversify'; import { ILogger } from '@theia/core/lib/common/logger'; -import * as rimraf from 'rimraf'; +import { rimraf } from 'rimraf'; export interface DownloadStorageItem { file: string; @@ -70,10 +70,8 @@ export class FileDownloadCache { } protected deleteRecursively(pathToDelete: string): void { - rimraf(pathToDelete, error => { - if (error) { - this.logger.warn(`An error occurred while deleting the temporary data from the disk. Cannot clean up: ${pathToDelete}.`, error); - } + rimraf(pathToDelete).catch(error => { + this.logger.warn(`An error occurred while deleting the temporary data from the disk. Cannot clean up: ${pathToDelete}.`, error); }); } diff --git a/packages/filesystem/src/node/download/file-download-endpoint.ts b/packages/filesystem/src/node/download/file-download-endpoint.ts index 6237c1e7ef050..a33523ef82bd0 100644 --- a/packages/filesystem/src/node/download/file-download-endpoint.ts +++ b/packages/filesystem/src/node/download/file-download-endpoint.ts @@ -21,7 +21,7 @@ import { injectable, inject, named } from '@theia/core/shared/inversify'; import { json } from 'body-parser'; import { Application, Router } from '@theia/core/shared/express'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileDownloadHandler } from './file-download-handler'; @injectable() diff --git a/packages/filesystem/src/node/download/file-download-handler.ts b/packages/filesystem/src/node/download/file-download-handler.ts index f1ae5e478c366..5ebf73e627da7 100644 --- a/packages/filesystem/src/node/download/file-download-handler.ts +++ b/packages/filesystem/src/node/download/file-download-handler.ts @@ -17,14 +17,14 @@ import * as os from 'os'; import * as fs from '@theia/core/shared/fs-extra'; import * as path from 'path'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import { Request, Response } from '@theia/core/shared/express'; import { inject, injectable } from '@theia/core/shared/inversify'; import { OK, BAD_REQUEST, METHOD_NOT_ALLOWED, NOT_FOUND, INTERNAL_SERVER_ERROR, REQUESTED_RANGE_NOT_SATISFIABLE, PARTIAL_CONTENT } from 'http-status-codes'; import URI from '@theia/core/lib/common/uri'; import { isEmpty } from '@theia/core/lib/common/objects'; import { ILogger } from '@theia/core/lib/common/logger'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { DirectoryArchiver } from './directory-archiver'; import { FileDownloadData } from '../../common/download/file-download-data'; import { FileDownloadCache, DownloadStorageItem } from './file-download-cache'; @@ -135,12 +135,12 @@ export abstract class FileDownloadHandler { end: (isNaN(end) || end > statSize - 1) ? (statSize - 1) : end }; } - protected async archive(inputPath: string, outputPath: string = path.join(os.tmpdir(), v4()), entries?: string[]): Promise { + protected async archive(inputPath: string, outputPath: string = path.join(os.tmpdir(), generateUuid()), entries?: string[]): Promise { await this.directoryArchiver.archive(inputPath, outputPath, entries); return outputPath; } - protected async createTempDir(downloadId: string = v4()): Promise { + protected async createTempDir(downloadId: string = generateUuid()): Promise { const outputPath = path.join(os.tmpdir(), downloadId); await fs.mkdir(outputPath); return outputPath; @@ -221,7 +221,7 @@ export class SingleFileDownloadHandler extends FileDownloadHandler { return; } try { - const downloadId = v4(); + const downloadId = generateUuid(); const options: PrepareDownloadOptions = { filePath, downloadId, remove: false }; if (!stat.isDirectory()) { await this.prepareDownload(request, response, options); @@ -271,7 +271,7 @@ export class MultiFileDownloadHandler extends FileDownloadHandler { } } try { - const downloadId = v4(); + const downloadId = generateUuid(); const outputRootPath = await this.createTempDir(downloadId); const distinctUris = Array.from(new Set(body.uris.map(uri => new URI(uri)))); const tarPaths = []; diff --git a/packages/filesystem/src/node/file-change-collection.spec.ts b/packages/filesystem/src/node/file-change-collection.spec.ts index 30f218a5e0eeb..a745e20899084 100644 --- a/packages/filesystem/src/node/file-change-collection.spec.ts +++ b/packages/filesystem/src/node/file-change-collection.spec.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import * as assert from 'assert'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileChangeCollection } from './file-change-collection'; import { FileChangeType } from '../common/files'; diff --git a/packages/filesystem/src/node/filesystem-backend-module.ts b/packages/filesystem/src/node/filesystem-backend-module.ts index abb8ea9773789..c59ba09a8bf3a 100644 --- a/packages/filesystem/src/node/filesystem-backend-module.ts +++ b/packages/filesystem/src/node/filesystem-backend-module.ts @@ -19,9 +19,9 @@ import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; import { ConnectionHandler, RpcConnectionHandler, ILogger } from '@theia/core/lib/common'; import { FileSystemWatcherServer, FileSystemWatcherService } from '../common/filesystem-watcher-protocol'; import { FileSystemWatcherServerClient } from './filesystem-watcher-client'; -import { NsfwFileSystemWatcherService, NsfwFileSystemWatcherServerOptions } from './nsfw-watcher/nsfw-filesystem-service'; +import { ParcelFileSystemWatcherService, ParcelFileSystemWatcherServerOptions } from './parcel-watcher/parcel-filesystem-service'; import { NodeFileUploadService } from './node-file-upload-service'; -import { NsfwOptions } from './nsfw-watcher/nsfw-options'; +import { ParcelWatcherOptions } from './parcel-watcher/parcel-options'; import { DiskFileSystemProvider } from './disk-file-system-provider'; import { remoteFileSystemPath, RemoteFileSystemServer, RemoteFileSystemClient, FileSystemProviderServer, RemoteFileSystemProxyFactory @@ -32,16 +32,16 @@ import { BackendApplicationContribution, IPCConnectionProvider } from '@theia/co import { RpcProxyFactory, ConnectionErrorHandler } from '@theia/core'; import { FileSystemWatcherServiceDispatcher } from './filesystem-watcher-dispatcher'; -export const NSFW_SINGLE_THREADED = process.argv.includes('--no-cluster'); -export const NSFW_WATCHER_VERBOSE = process.argv.includes('--nsfw-watcher-verbose'); +export const WATCHER_SINGLE_THREADED = process.argv.includes('--no-cluster'); +export const WATCHER_VERBOSE = process.argv.includes('--watcher-verbose'); -export const NsfwFileSystemWatcherServiceProcessOptions = Symbol('NsfwFileSystemWatcherServiceProcessOptions'); +export const FileSystemWatcherServiceProcessOptions = Symbol('FileSystemWatcherServiceProcessOptions'); /** - * Options to control the way the `NsfwFileSystemWatcherService` process is spawned. + * Options to control the way the `ParcelFileSystemWatcherService` process is spawned. */ -export interface NsfwFileSystemWatcherServiceProcessOptions { +export interface FileSystemWatcherServiceProcessOptions { /** - * Path to the script that will run the `NsfwFileSystemWatcherService` in a new process. + * Path to the script that will run the `ParcelFileSystemWatcherService` in a new process. */ entryPoint: string; } @@ -66,41 +66,41 @@ export default new ContainerModule(bind => { }); export function bindFileSystemWatcherServer(bind: interfaces.Bind): void { - bind(NsfwOptions).toConstantValue({}); + bind(ParcelWatcherOptions).toConstantValue({}); bind(FileSystemWatcherServiceDispatcher).toSelf().inSingletonScope(); bind(FileSystemWatcherServerClient).toSelf(); bind(FileSystemWatcherServer).toService(FileSystemWatcherServerClient); - bind(NsfwFileSystemWatcherServiceProcessOptions).toDynamicValue(ctx => ({ - entryPoint: path.join(__dirname, 'nsfw-watcher'), + bind(FileSystemWatcherServiceProcessOptions).toDynamicValue(ctx => ({ + entryPoint: path.join(__dirname, 'parcel-watcher'), })).inSingletonScope(); - bind(NsfwFileSystemWatcherServerOptions).toDynamicValue(ctx => { + bind(ParcelFileSystemWatcherServerOptions).toDynamicValue(ctx => { const logger = ctx.container.get(ILogger); - const nsfwOptions = ctx.container.get(NsfwOptions); + const watcherOptions = ctx.container.get(ParcelWatcherOptions); return { - nsfwOptions, - verbose: NSFW_WATCHER_VERBOSE, + parcelOptions: watcherOptions, + verbose: WATCHER_VERBOSE, info: (message, ...args) => logger.info(message, ...args), error: (message, ...args) => logger.error(message, ...args), }; }).inSingletonScope(); bind(FileSystemWatcherService).toDynamicValue( - ctx => NSFW_SINGLE_THREADED - ? createNsfwFileSystemWatcherService(ctx) - : spawnNsfwFileSystemWatcherServiceProcess(ctx) + ctx => WATCHER_SINGLE_THREADED + ? createParcelFileSystemWatcherService(ctx) + : spawnParcelFileSystemWatcherServiceProcess(ctx) ).inSingletonScope(); } /** * Run the watch server in the current process. */ -export function createNsfwFileSystemWatcherService(ctx: interfaces.Context): FileSystemWatcherService { - const options = ctx.container.get(NsfwFileSystemWatcherServerOptions); +export function createParcelFileSystemWatcherService(ctx: interfaces.Context): FileSystemWatcherService { + const options = ctx.container.get(ParcelFileSystemWatcherServerOptions); const dispatcher = ctx.container.get(FileSystemWatcherServiceDispatcher); - const server = new NsfwFileSystemWatcherService(options); + const server = new ParcelFileSystemWatcherService(options); server.setClient(dispatcher); return server; } @@ -109,21 +109,21 @@ export function createNsfwFileSystemWatcherService(ctx: interfaces.Context): Fil * Run the watch server in a child process. * Return a proxy forwarding calls to the child process. */ -export function spawnNsfwFileSystemWatcherServiceProcess(ctx: interfaces.Context): FileSystemWatcherService { - const options = ctx.container.get(NsfwFileSystemWatcherServiceProcessOptions); +export function spawnParcelFileSystemWatcherServiceProcess(ctx: interfaces.Context): FileSystemWatcherService { + const options = ctx.container.get(FileSystemWatcherServiceProcessOptions); const dispatcher = ctx.container.get(FileSystemWatcherServiceDispatcher); - const serverName = 'nsfw-watcher'; + const serverName = 'parcel-watcher'; const logger = ctx.container.get(ILogger); - const nsfwOptions = ctx.container.get(NsfwOptions); + const watcherOptions = ctx.container.get(ParcelWatcherOptions); const ipcConnectionProvider = ctx.container.get(IPCConnectionProvider); const proxyFactory = new RpcProxyFactory(); const serverProxy = proxyFactory.createProxy(); // We need to call `.setClient` before listening, else the JSON-RPC calls won't go through. serverProxy.setClient(dispatcher); const args: string[] = [ - `--nsfwOptions=${JSON.stringify(nsfwOptions)}` + `--watchOptions=${JSON.stringify(watcherOptions)}` ]; - if (NSFW_WATCHER_VERBOSE) { + if (WATCHER_VERBOSE) { args.push('--verbose'); } ipcConnectionProvider.listen({ diff --git a/packages/filesystem/src/node/filesystem-watcher-client.ts b/packages/filesystem/src/node/filesystem-watcher-client.ts index ce44d8099fedd..e720b9adaca9f 100644 --- a/packages/filesystem/src/node/filesystem-watcher-client.ts +++ b/packages/filesystem/src/node/filesystem-watcher-client.ts @@ -18,10 +18,8 @@ import { injectable, inject } from '@theia/core/shared/inversify'; import { FileSystemWatcherServer, WatchOptions, FileSystemWatcherClient, FileSystemWatcherService } from '../common/filesystem-watcher-protocol'; import { FileSystemWatcherServiceDispatcher } from './filesystem-watcher-dispatcher'; -export const NSFW_WATCHER = 'nsfw-watcher'; - /** - * Wraps the NSFW singleton service for each frontend. + * Wraps the watcher singleton service for each frontend. */ @injectable() export class FileSystemWatcherServerClient implements FileSystemWatcherServer { diff --git a/packages/filesystem/src/node/node-file-upload-service.ts b/packages/filesystem/src/node/node-file-upload-service.ts index efd063348ecde..9687ad761bd07 100644 --- a/packages/filesystem/src/node/node-file-upload-service.ts +++ b/packages/filesystem/src/node/node-file-upload-service.ts @@ -73,7 +73,13 @@ export class NodeFileUploadService implements BackendApplicationContribution { response.status(200).send(target); // ok } catch (error) { console.error(error); - response.sendStatus(500); // internal server error + if (error.message) { + // internal server error with error message as response + response.status(500).send(error.message); + } else { + // default internal server error + response.sendStatus(500); + } } } diff --git a/packages/filesystem/src/node/nsfw-watcher/index.ts b/packages/filesystem/src/node/parcel-watcher/index.ts similarity index 90% rename from packages/filesystem/src/node/nsfw-watcher/index.ts rename to packages/filesystem/src/node/parcel-watcher/index.ts index 92920b9aaa48b..0edbb8a2a0277 100644 --- a/packages/filesystem/src/node/nsfw-watcher/index.ts +++ b/packages/filesystem/src/node/parcel-watcher/index.ts @@ -17,7 +17,7 @@ import * as yargs from '@theia/core/shared/yargs'; import { RpcProxyFactory } from '@theia/core'; import { FileSystemWatcherServiceClient } from '../../common/filesystem-watcher-protocol'; -import { NsfwFileSystemWatcherService } from './nsfw-filesystem-service'; +import { ParcelFileSystemWatcherService } from './parcel-filesystem-service'; import { IPCEntryPoint } from '@theia/core/lib/node/messaging/ipc-protocol'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -30,7 +30,7 @@ const options: { alias: 'v', type: 'boolean' }) - .option('nsfwOptions', { + .option('watchOptions', { alias: 'o', type: 'string', coerce: JSON.parse @@ -38,7 +38,7 @@ const options: { .argv as any; export default (connection => { - const server = new NsfwFileSystemWatcherService(options); + const server = new ParcelFileSystemWatcherService(options); const factory = new RpcProxyFactory(server); server.setClient(factory.createProxy()); factory.listen(connection); diff --git a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-service.ts b/packages/filesystem/src/node/parcel-watcher/parcel-filesystem-service.ts similarity index 75% rename from packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-service.ts rename to packages/filesystem/src/node/parcel-watcher/parcel-filesystem-service.ts index 6e4723076fea3..bd0c352c607e3 100644 --- a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-service.ts +++ b/packages/filesystem/src/node/parcel-watcher/parcel-filesystem-service.ts @@ -14,29 +14,29 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import nsfw = require('@theia/core/shared/nsfw'); import path = require('path'); import { promises as fsp } from 'fs'; import { IMinimatch, Minimatch } from 'minimatch'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileChangeType, FileSystemWatcherService, FileSystemWatcherServiceClient, WatchOptions } from '../../common/filesystem-watcher-protocol'; import { FileChangeCollection } from '../file-change-collection'; import { Deferred, timeout } from '@theia/core/lib/common/promise-util'; +import { subscribe, Options, AsyncSubscription, Event } from '@theia/core/shared/@parcel/watcher'; -export interface NsfwWatcherOptions { +export interface ParcelWatcherOptions { ignored: IMinimatch[] } -export const NsfwFileSystemWatcherServerOptions = Symbol('NsfwFileSystemWatcherServerOptions'); -export interface NsfwFileSystemWatcherServerOptions { +export const ParcelFileSystemWatcherServerOptions = Symbol('ParcelFileSystemWatcherServerOptions'); +export interface ParcelFileSystemWatcherServerOptions { verbose: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any info: (message: string, ...args: any[]) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any error: (message: string, ...args: any[]) => void; - nsfwOptions: nsfw.Options; + parcelOptions: Options; } /** @@ -54,7 +54,7 @@ export const WatcherDisposal = Symbol('WatcherDisposal'); * Once there are no more references the handle * will wait for some time before destroying its resources. */ -export class NsfwWatcher { +export class ParcelWatcher { protected static debugIdSequence = 0; @@ -63,12 +63,12 @@ export class NsfwWatcher { /** * Used for debugging to keep track of the watchers. */ - protected debugId = NsfwWatcher.debugIdSequence++; + protected debugId = ParcelWatcher.debugIdSequence++; /** - * When this field is set, it means the nsfw instance was successfully started. + * When this field is set, it means the watcher instance was successfully started. */ - protected nsfw: nsfw.NSFW | undefined; + protected watcher: AsyncSubscription | undefined; /** * When the ref count hits zero, we schedule this watch handle to be disposed. @@ -93,7 +93,7 @@ export class NsfwWatcher { * Ensures that events are processed in the order they are emitted, * despite being processed async. */ - protected nsfwEventProcessingQueue: Promise = Promise.resolve(); + protected parcelEventProcessingQueue: Promise = Promise.resolve(); /** * Resolves once this handle disposed itself and its resources. Never throws. @@ -115,9 +115,9 @@ export class NsfwWatcher { /** Filesystem path to be watched. */ readonly fsPath: string, /** Watcher-specific options */ - readonly watcherOptions: NsfwWatcherOptions, - /** Logging and Nsfw options */ - protected readonly nsfwFileSystemWatchServerOptions: NsfwFileSystemWatcherServerOptions, + readonly watcherOptions: ParcelWatcherOptions, + /** Logging and parcel watcher options */ + protected readonly parcelFileSystemWatchServerOptions: ParcelFileSystemWatcherServerOptions, /** The client to forward events to. */ protected readonly fileSystemWatcherClient: FileSystemWatcherServiceClient, /** Amount of time in ms to wait once this handle is not referenced anymore. */ @@ -207,7 +207,7 @@ export class NsfwWatcher { /** * When starting a watcher, we'll first check and wait for the path to exists - * before running an NSFW watcher. + * before running a parcel watcher. */ protected async start(): Promise { while (await fsp.stat(this.fsPath).then(() => false, () => true)) { @@ -215,71 +215,61 @@ export class NsfwWatcher { this.assertNotDisposed(); } this.assertNotDisposed(); - const watcher = await this.createNsfw(); + const watcher = await this.createWatcher(); this.assertNotDisposed(); - await watcher.start(); this.debug('STARTED', `disposed=${this.disposed}`); // The watcher could be disposed while it was starting, make sure to check for this: if (this.disposed) { - await this.stopNsfw(watcher); + await this.stopWatcher(watcher); throw WatcherDisposal; } - this.nsfw = watcher; + this.watcher = watcher; } /** - * Given a started nsfw instance, gracefully shut it down. + * Given a started parcel watcher instance, gracefully shut it down. */ - protected async stopNsfw(watcher: nsfw.NSFW): Promise { - await watcher.stop() + protected async stopWatcher(watcher: AsyncSubscription): Promise { + await watcher.unsubscribe() .then(() => 'success=true', error => error) .then(status => this.debug('STOPPED', status)); } - protected async createNsfw(): Promise { - const fsPath = await fsp.realpath(this.fsPath); - return nsfw(fsPath, events => this.handleNsfwEvents(events), { - ...this.nsfwFileSystemWatchServerOptions.nsfwOptions, - // The errorCallback is called whenever NSFW crashes *while* watching. - // See https://github.com/atom/github/issues/342 - errorCallback: error => { - console.error(`NSFW service error on "${fsPath}":`, error); + protected async createWatcher(): Promise { + let fsPath = await fsp.realpath(this.fsPath); + if ((await fsp.stat(fsPath)).isFile()) { + fsPath = path.dirname(fsPath); + } + return subscribe(fsPath, (err, events) => { + if (err) { + console.error(`Watcher service error on "${fsPath}":`, err); this._dispose(); this.fireError(); - // Make sure to call user's error handling code: - if (this.nsfwFileSystemWatchServerOptions.nsfwOptions.errorCallback) { - this.nsfwFileSystemWatchServerOptions.nsfwOptions.errorCallback(error); - } - }, + return; + } + this.handleWatcherEvents(events); + }, { + ...this.parcelFileSystemWatchServerOptions.parcelOptions }); } - protected handleNsfwEvents(events: nsfw.FileChangeEvent[]): void { + protected handleWatcherEvents(events: Event[]): void { // Only process events if someone is listening. if (this.isInUse()) { - // This callback is async, but nsfw won't wait for it to finish before firing the next one. + // This callback is async, but parcel won't wait for it to finish before firing the next one. // We will use a lock/queue to make sure everything is processed in the order it arrives. - this.nsfwEventProcessingQueue = this.nsfwEventProcessingQueue.then(async () => { + this.parcelEventProcessingQueue = this.parcelEventProcessingQueue.then(async () => { const fileChangeCollection = new FileChangeCollection(); - await Promise.all(events.map(async event => { - if (event.action === nsfw.actions.RENAMED) { - const [oldPath, newPath] = await Promise.all([ - this.resolveEventPath(event.directory, event.oldFile), - this.resolveEventPath(event.newDirectory, event.newFile), - ]); - this.pushFileChange(fileChangeCollection, FileChangeType.DELETED, oldPath); - this.pushFileChange(fileChangeCollection, FileChangeType.ADDED, newPath); - } else { - const filePath = await this.resolveEventPath(event.directory, event.file!); - if (event.action === nsfw.actions.CREATED) { - this.pushFileChange(fileChangeCollection, FileChangeType.ADDED, filePath); - } else if (event.action === nsfw.actions.DELETED) { - this.pushFileChange(fileChangeCollection, FileChangeType.DELETED, filePath); - } else if (event.action === nsfw.actions.MODIFIED) { - this.pushFileChange(fileChangeCollection, FileChangeType.UPDATED, filePath); - } + for (const event of events) { + const filePath = event.path; + if (event.type === 'create') { + this.pushFileChange(fileChangeCollection, FileChangeType.ADDED, filePath); + } else if (event.type === 'delete') { + this.pushFileChange(fileChangeCollection, FileChangeType.DELETED, filePath); + } else if (event.type === 'update') { + this.pushFileChange(fileChangeCollection, FileChangeType.UPDATED, filePath); } - })); + } const changes = fileChangeCollection.values(); // If all changes are part of the ignored files, the collection will be empty. if (changes.length > 0) { @@ -293,7 +283,7 @@ export class NsfwWatcher { } protected async resolveEventPath(directory: string, file: string): Promise { - // nsfw already resolves symlinks, the paths should be clean already: + // parcel already resolves symlinks, the paths should be clean already: return path.resolve(directory, file); } @@ -344,9 +334,9 @@ export class NsfwWatcher { if (!this.disposed) { this.disposed = true; this.deferredDisposalDeferred.reject(WatcherDisposal); - if (this.nsfw) { - this.stopNsfw(this.nsfw); - this.nsfw = undefined; + if (this.watcher) { + this.stopWatcher(this.watcher); + this.watcher = undefined; } this.debug('DISPOSED'); } @@ -354,12 +344,12 @@ export class NsfwWatcher { // eslint-disable-next-line @typescript-eslint/no-explicit-any protected info(prefix: string, ...params: any[]): void { - this.nsfwFileSystemWatchServerOptions.info(`${prefix} NsfwWatcher(${this.debugId} at "${this.fsPath}"):`, ...params); + this.parcelFileSystemWatchServerOptions.info(`${prefix} ParcelWatcher(${this.debugId} at "${this.fsPath}"):`, ...params); } // eslint-disable-next-line @typescript-eslint/no-explicit-any protected debug(prefix: string, ...params: any[]): void { - if (this.nsfwFileSystemWatchServerOptions.verbose) { + if (this.parcelFileSystemWatchServerOptions.verbose) { this.info(prefix, ...params); } } @@ -370,20 +360,20 @@ export class NsfwWatcher { * * This watcherId will map to this handle type which keeps track of the clientId that made the request. */ -export interface NsfwWatcherHandle { +export interface PacelWatcherHandle { clientId: number; - watcher: NsfwWatcher; + watcher: ParcelWatcher; } -export class NsfwFileSystemWatcherService implements FileSystemWatcherService { +export class ParcelFileSystemWatcherService implements FileSystemWatcherService { protected client: FileSystemWatcherServiceClient | undefined; protected watcherId = 0; - protected readonly watchers = new Map(); - protected readonly watcherHandles = new Map(); + protected readonly watchers = new Map(); + protected readonly watcherHandles = new Map(); - protected readonly options: NsfwFileSystemWatcherServerOptions; + protected readonly options: ParcelFileSystemWatcherServerOptions; /** * `this.client` is undefined until someone sets it. @@ -393,9 +383,9 @@ export class NsfwFileSystemWatcherService implements FileSystemWatcherService { onError: event => this.client?.onError(event), }; - constructor(options?: Partial) { + constructor(options?: Partial) { this.options = { - nsfwOptions: {}, + parcelOptions: {}, verbose: false, info: (message, ...args) => console.info(message, ...args), error: (message, ...args) => console.error(message, ...args), @@ -430,12 +420,12 @@ export class NsfwFileSystemWatcherService implements FileSystemWatcherService { return watcherId; } - protected createWatcher(clientId: number, fsPath: string, options: WatchOptions): NsfwWatcher { - const watcherOptions: NsfwWatcherOptions = { + protected createWatcher(clientId: number, fsPath: string, options: WatchOptions): ParcelWatcher { + const watcherOptions: ParcelWatcherOptions = { ignored: options.ignored .map(pattern => new Minimatch(pattern, { dot: true })), }; - return new NsfwWatcher(clientId, fsPath, watcherOptions, this.options, this.maybeClient); + return new ParcelWatcher(clientId, fsPath, watcherOptions, this.options, this.maybeClient); } async unwatchFileChanges(watcherId: number): Promise { diff --git a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.spec.ts b/packages/filesystem/src/node/parcel-watcher/parcel-filesystem-watcher.spec.ts similarity index 82% rename from packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.spec.ts rename to packages/filesystem/src/node/parcel-watcher/parcel-filesystem-watcher.spec.ts index 6e9b9e90298f9..35ec061ca1e7b 100644 --- a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.spec.ts +++ b/packages/filesystem/src/node/parcel-watcher/parcel-filesystem-watcher.spec.ts @@ -21,16 +21,16 @@ import * as fs from '@theia/core/shared/fs-extra'; import * as assert from 'assert'; import URI from '@theia/core/lib/common/uri'; import { FileUri } from '@theia/core/lib/node'; -import { NsfwFileSystemWatcherService } from './nsfw-filesystem-service'; +import { ParcelFileSystemWatcherService } from './parcel-filesystem-service'; import { DidFilesChangedParams, FileChange, FileChangeType } from '../../common/filesystem-watcher-protocol'; const expect = chai.expect; const track = temp.track(); -describe('nsfw-filesystem-watcher', function (): void { +describe('parcel-filesystem-watcher', function (): void { let root: URI; - let watcherService: NsfwFileSystemWatcherService; + let watcherService: ParcelFileSystemWatcherService; let watcherId: number; this.timeout(100000); @@ -38,7 +38,7 @@ describe('nsfw-filesystem-watcher', function (): void { beforeEach(async () => { let tempPath = temp.mkdirSync('node-fs-root'); // Sometimes tempPath will use some Windows 8.3 short name in its path. This is a problem - // since NSFW always returns paths with long names. We need to convert here. + // since parcel always returns paths with long names. We need to convert here. // See: https://stackoverflow.com/a/34473971/7983255 if (process.platform === 'win32') { tempPath = cp.execSync(`powershell "(Get-Item -LiteralPath '${tempPath}').FullName"`, { @@ -46,9 +46,9 @@ describe('nsfw-filesystem-watcher', function (): void { }).trim(); } root = FileUri.create(fs.realpathSync(tempPath)); - watcherService = createNsfwFileSystemWatcherService(); + watcherService = createParcelFileSystemWatcherService(); watcherId = await watcherService.watchFileChanges(0, root.toString()); - await sleep(2000); + await sleep(200); }); afterEach(async () => { @@ -76,15 +76,15 @@ describe('nsfw-filesystem-watcher', function (): void { fs.mkdirSync(FileUri.fsPath(root.resolve('foo'))); expect(fs.statSync(FileUri.fsPath(root.resolve('foo'))).isDirectory()).to.be.true; - await sleep(2000); + await sleep(200); fs.mkdirSync(FileUri.fsPath(root.resolve('foo').resolve('bar'))); expect(fs.statSync(FileUri.fsPath(root.resolve('foo').resolve('bar'))).isDirectory()).to.be.true; - await sleep(2000); + await sleep(200); fs.writeFileSync(FileUri.fsPath(root.resolve('foo').resolve('bar').resolve('baz.txt')), 'baz'); expect(fs.readFileSync(FileUri.fsPath(root.resolve('foo').resolve('bar').resolve('baz.txt')), 'utf8')).to.be.equal('baz'); - await sleep(2000); + await sleep(200); assert.deepStrictEqual([...actualUris], expectedUris); }); @@ -106,20 +106,20 @@ describe('nsfw-filesystem-watcher', function (): void { fs.mkdirSync(FileUri.fsPath(root.resolve('foo'))); expect(fs.statSync(FileUri.fsPath(root.resolve('foo'))).isDirectory()).to.be.true; - await sleep(2000); + await sleep(200); fs.mkdirSync(FileUri.fsPath(root.resolve('foo').resolve('bar'))); expect(fs.statSync(FileUri.fsPath(root.resolve('foo').resolve('bar'))).isDirectory()).to.be.true; - await sleep(2000); + await sleep(200); fs.writeFileSync(FileUri.fsPath(root.resolve('foo').resolve('bar').resolve('baz.txt')), 'baz'); expect(fs.readFileSync(FileUri.fsPath(root.resolve('foo').resolve('bar').resolve('baz.txt')), 'utf8')).to.be.equal('baz'); - await sleep(2000); + await sleep(200); assert.deepStrictEqual(actualUris.size, 0); }); - it('Renaming should emit a DELETED change followed by ADDED', async function (): Promise { + it('Renaming should emit a DELETED and ADDED event', async function (): Promise { const file_txt = root.resolve('file.txt'); const FILE_txt = root.resolve('FILE.txt'); const changes: FileChange[] = []; @@ -131,41 +131,34 @@ describe('nsfw-filesystem-watcher', function (): void { FileUri.fsPath(file_txt), 'random content\n' ); - await sleep(1000); + await sleep(200); await fs.promises.rename( FileUri.fsPath(file_txt), FileUri.fsPath(FILE_txt) ); - await sleep(1000); + await sleep(200); + // The order of DELETED and ADDED is not deterministic try { expect(changes).deep.eq([ // initial file creation change event: { type: FileChangeType.ADDED, uri: file_txt.toString() }, // rename change events: { type: FileChangeType.DELETED, uri: file_txt.toString() }, - { type: FileChangeType.ADDED, uri: FILE_txt.toString() } + { type: FileChangeType.ADDED, uri: FILE_txt.toString() }, ]); - } catch (error) { - // TODO: remove this try/catch once the bug on macOS is fixed. - // See https://github.com/Axosoft/nsfw/issues/146 - if (process.platform !== 'darwin') { - throw error; - } - // On macOS we only get ADDED events for some reason + } catch { expect(changes).deep.eq([ // initial file creation change event: { type: FileChangeType.ADDED, uri: file_txt.toString() }, // rename change events: - { type: FileChangeType.ADDED, uri: file_txt.toString() }, - { type: FileChangeType.ADDED, uri: FILE_txt.toString() } + { type: FileChangeType.ADDED, uri: FILE_txt.toString() }, + { type: FileChangeType.DELETED, uri: file_txt.toString() }, ]); - // Mark the test case as skipped so it stands out that the bogus branch got tested - this.skip(); } }); - function createNsfwFileSystemWatcherService(): NsfwFileSystemWatcherService { - return new NsfwFileSystemWatcherService({ + function createParcelFileSystemWatcherService(): ParcelFileSystemWatcherService { + return new ParcelFileSystemWatcherService({ verbose: true }); } diff --git a/packages/filesystem/src/node/nsfw-watcher/nsfw-options.ts b/packages/filesystem/src/node/parcel-watcher/parcel-options.ts similarity index 80% rename from packages/filesystem/src/node/nsfw-watcher/nsfw-options.ts rename to packages/filesystem/src/node/parcel-watcher/parcel-options.ts index f658c4bb4e7ed..b52c0794553cc 100644 --- a/packages/filesystem/src/node/nsfw-watcher/nsfw-options.ts +++ b/packages/filesystem/src/node/parcel-watcher/parcel-options.ts @@ -14,10 +14,10 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import * as nsfw from '@theia/core/shared/nsfw'; +import { Options } from '@theia/core/shared/@parcel/watcher'; /** - * Inversify service identifier allowing extensions to override options passed to nsfw by the file watcher. + * Inversify service identifier allowing extensions to override options passed to parcel by the file watcher. */ -export const NsfwOptions = Symbol('NsfwOptions'); -export type NsfwOptions = nsfw.Options; +export const ParcelWatcherOptions = Symbol('ParcelWatcherOptions'); +export type ParcelWatcherOptions = Options; diff --git a/packages/getting-started/package.json b/packages/getting-started/package.json index d7ca8fdcc93a9..cb666bfd2dc5f 100644 --- a/packages/getting-started/package.json +++ b/packages/getting-started/package.json @@ -1,14 +1,15 @@ { "name": "@theia/getting-started", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - GettingStarted Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/keymaps": "1.44.0", - "@theia/preview": "1.44.0", - "@theia/workspace": "1.44.0" + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/keymaps": "1.54.0", + "@theia/preview": "1.54.0", + "@theia/workspace": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -44,7 +45,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/getting-started/src/browser/getting-started-frontend-module.ts b/packages/getting-started/src/browser/getting-started-frontend-module.ts index fbcc828646f02..d029dd7a2236c 100644 --- a/packages/getting-started/src/browser/getting-started-frontend-module.ts +++ b/packages/getting-started/src/browser/getting-started-frontend-module.ts @@ -17,13 +17,14 @@ import { GettingStartedContribution } from './getting-started-contribution'; import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; import { GettingStartedWidget } from './getting-started-widget'; -import { WidgetFactory, FrontendApplicationContribution, bindViewContribution } from '@theia/core/lib/browser'; +import { WidgetFactory, FrontendApplicationContribution, bindViewContribution, noopWidgetStatusBarContribution, WidgetStatusBarContribution } from '@theia/core/lib/browser'; import { bindGettingStartedPreferences } from './getting-started-preferences'; import '../../src/browser/style/index.css'; export default new ContainerModule((bind: interfaces.Bind) => { bindViewContribution(bind, GettingStartedContribution); bind(FrontendApplicationContribution).toService(GettingStartedContribution); + bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(GettingStartedWidget)); bind(GettingStartedWidget).toSelf(); bind(WidgetFactory).toDynamicValue(context => ({ id: GettingStartedWidget.ID, diff --git a/packages/getting-started/src/browser/getting-started-widget.tsx b/packages/getting-started/src/browser/getting-started-widget.tsx index 22fa83fe8d769..137ddb783bc7e 100644 --- a/packages/getting-started/src/browser/getting-started-widget.tsx +++ b/packages/getting-started/src/browser/getting-started-widget.tsx @@ -14,18 +14,18 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import * as React from '@theia/core/shared/react'; -import URI from '@theia/core/lib/common/uri'; -import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; -import { CommandRegistry, isOSX, environment, Path } from '@theia/core/lib/common'; -import { WorkspaceCommands, WorkspaceService } from '@theia/workspace/lib/browser'; -import { KeymapsCommands } from '@theia/keymaps/lib/browser'; -import { Message, ReactWidget, CommonCommands, LabelProvider, Key, KeyCode, codicon, PreferenceService } from '@theia/core/lib/browser'; -import { ApplicationInfo, ApplicationServer } from '@theia/core/lib/common/application-protocol'; +import { codicon, CommonCommands, Key, KeyCode, LabelProvider, Message, PreferenceService, ReactWidget } from '@theia/core/lib/browser'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { CommandRegistry, environment, isOSX, Path } from '@theia/core/lib/common'; +import { ApplicationInfo, ApplicationServer } from '@theia/core/lib/common/application-protocol'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { nls } from '@theia/core/lib/common/nls'; +import URI from '@theia/core/lib/common/uri'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; +import { KeymapsCommands } from '@theia/keymaps/lib/browser'; +import { WorkspaceCommands, WorkspaceService } from '@theia/workspace/lib/browser'; /** * Default implementation of the `GettingStartedWidget`. @@ -71,6 +71,11 @@ export class GettingStartedWidget extends ReactWidget { */ protected recentWorkspaces: string[] = []; + /** + * Indicates whether the "ai-core" extension is available. + */ + protected aiIsIncluded: boolean; + /** * Collection of useful links to display for end users. */ @@ -78,6 +83,8 @@ export class GettingStartedWidget extends ReactWidget { protected readonly compatibilityUrl = 'https://eclipse-theia.github.io/vscode-theia-comparator/status.html'; protected readonly extensionUrl = 'https://www.theia-ide.org/docs/authoring_extensions'; protected readonly pluginUrl = 'https://www.theia-ide.org/docs/authoring_plugins'; + protected readonly theiaAIDocUrl = 'https://theia-ide.org/docs/user_ai/'; + protected readonly ghProjectUrl = 'https://github.com/eclipse-theia/theia/issues/new/choose'; @inject(ApplicationServer) protected readonly appServer: ApplicationServer; @@ -114,6 +121,9 @@ export class GettingStartedWidget extends ReactWidget { this.applicationInfo = await this.appServer.getApplicationInfo(); this.recentWorkspaces = await this.workspaceService.recentWorkspaces(); this.home = new URI(await this.environments.getHomeDirUri()).path.toString(); + + const extensions = await this.appServer.getExtensionsInfos(); + this.aiIsIncluded = extensions.find(ext => ext.name === '@theia/ai-core') !== undefined; this.update(); } @@ -131,11 +141,16 @@ export class GettingStartedWidget extends ReactWidget { protected render(): React.ReactNode { return
    + {this.aiIsIncluded && +
    + {this.renderAIBanner()} +
    + } {this.renderHeader()}
    - {this.renderOpen()} + {this.renderStart()}
    @@ -176,12 +191,22 @@ export class GettingStartedWidget extends ReactWidget { } /** - * Render the `open` section. - * Displays a collection of `open` commands. + * Render the `Start` section. + * Displays a collection of "start-to-work" related commands like `open` commands and some other. */ - protected renderOpen(): React.ReactNode { + protected renderStart(): React.ReactNode { const requireSingleOpen = isOSX || !environment.electron.is(); + const createFile = ; + const open = requireSingleOpen &&
    -

    {nls.localizeByDefault('Open')}

    +

    {nls.localizeByDefault('Start')}

    + {createFile} {open} {openFile} {openFolder} @@ -376,6 +402,66 @@ export class GettingStartedWidget extends ReactWidget { return ; } + protected renderAIBanner(): React.ReactNode { + return
    +
    +
    +

    🚀 AI Support in the Theia IDE is available! [Experimental] ✨

    +
    +
    + Theia IDE now contains experimental AI support, which offers early access to cutting-edge AI capabilities within your IDE. +
    +
    + Please note that these features are disabled by default, ensuring that users can opt-in at their discretion. + For those who choose to enable AI support, it is important to be aware that these experimental features may generate continuous + requests to the language models (LLMs) you provide access to. This might incur costs that you need to monitor closely. +
    + For more details, please visit   +
    this.doOpenExternalLink(this.theiaAIDocUrl)} + onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.theiaAIDocUrl)}> + {'the documentation'} + . +
    +
    + 🚧 Please note that this feature is currently in development and may undergo frequent changes. + We welcome your feedback, contributions, and sponsorship! To support the ongoing development of the AI capabilities please visit the  + this.doOpenExternalLink(this.ghProjectUrl)} + onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.ghProjectUrl)}> + {'Github Project'} + . +  Thank you for being part of our community! +
    +
    + +
    +
    +
    +
    +
    ; + } + + protected doOpenAIChatView = () => this.commandRegistry.executeCommand('aiChat:toggle'); + protected doOpenAIChatViewEnter = (e: React.KeyboardEvent) => { + if (this.isEnterKey(e)) { + this.doOpenAIChatView(); + } + }; + /** * Build the list of workspace paths. * @param workspaces {string[]} the list of workspaces. @@ -392,6 +478,16 @@ export class GettingStartedWidget extends ReactWidget { return paths; } + /** + * Trigger the create file command. + */ + protected doCreateFile = () => this.commandRegistry.executeCommand(CommonCommands.NEW_UNTITLED_FILE.id); + protected doCreateFileEnter = (e: React.KeyboardEvent) => { + if (this.isEnterKey(e)) { + this.doCreateFile(); + } + }; + /** * Trigger the open command. */ diff --git a/packages/getting-started/src/browser/style/index.css b/packages/getting-started/src/browser/style/index.css index 17216da4df9da..274fefe468e47 100644 --- a/packages/getting-started/src/browser/style/index.css +++ b/packages/getting-started/src/browser/style/index.css @@ -107,3 +107,23 @@ body { display: flex; align-items: center; } + +.gs-float { + float: right; + width: 50%; + margin-top: 100px; +} + +.gs-container.gs-experimental-container { + border: 1px solid var(--theia-focusBorder); + padding: 15px; +} + +.shadow-pulse { + animation: shadowPulse 2s infinite ease-in-out; +} + +@keyframes shadowPulse { + 0%, 100% { box-shadow: 0 0 0 rgba(0, 0, 0, 0); } + 50% { box-shadow: 0 0 15px rgba(0, 0, 0, 0.4); } +} diff --git a/packages/git/package.json b/packages/git/package.json index 0671f6f1df151..a5e1b776c1ec3 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -1,19 +1,19 @@ { "name": "@theia/git", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Git Integration", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/navigator": "1.44.0", - "@theia/scm": "1.44.0", - "@theia/scm-extra": "1.44.0", - "@theia/workspace": "1.44.0", - "@types/diff": "^3.2.2", + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", + "@theia/navigator": "1.54.0", + "@theia/scm": "1.54.0", + "@theia/scm-extra": "1.54.0", + "@theia/workspace": "1.54.0", + "@types/diff": "^5.2.1", "@types/p-queue": "^2.3.1", - "diff": "^3.4.0", + "diff": "^5.2.0", "dugite-extra": "0.1.17", "find-git-exec": "^0.0.4", "find-git-repositories": "^0.1.1", @@ -21,7 +21,8 @@ "node-ssh": "^12.0.1", "octicons": "^7.1.0", "p-queue": "^2.4.2", - "ts-md5": "^1.2.2" + "ts-md5": "^1.2.2", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -66,7 +67,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0", + "@theia/ext-scripts": "1.54.0", "@types/luxon": "^2.3.2", "upath": "^1.0.2" }, diff --git a/packages/git/src/browser/dirty-diff/dirty-diff-contribution.ts b/packages/git/src/browser/dirty-diff/dirty-diff-contribution.ts index 0ffc8a4148dfe..ced8be176d5bb 100644 --- a/packages/git/src/browser/dirty-diff/dirty-diff-contribution.ts +++ b/packages/git/src/browser/dirty-diff/dirty-diff-contribution.ts @@ -16,6 +16,7 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { DirtyDiffDecorator } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-decorator'; +import { DirtyDiffNavigator } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-navigator'; import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser'; import { DirtyDiffManager } from './dirty-diff-manager'; @@ -25,10 +26,13 @@ export class DirtyDiffContribution implements FrontendApplicationContribution { constructor( @inject(DirtyDiffManager) protected readonly dirtyDiffManager: DirtyDiffManager, @inject(DirtyDiffDecorator) protected readonly dirtyDiffDecorator: DirtyDiffDecorator, + @inject(DirtyDiffNavigator) protected readonly dirtyDiffNavigator: DirtyDiffNavigator, ) { } onStart(app: FrontendApplication): void { - this.dirtyDiffManager.onDirtyDiffUpdate(update => this.dirtyDiffDecorator.applyDecorations(update)); + this.dirtyDiffManager.onDirtyDiffUpdate(update => { + this.dirtyDiffDecorator.applyDecorations(update); + this.dirtyDiffNavigator.handleDirtyDiffUpdate(update); + }); } - } diff --git a/packages/git/src/browser/dirty-diff/dirty-diff-manager.ts b/packages/git/src/browser/dirty-diff/dirty-diff-manager.ts index 714ce0f12f943..31ede34ee21f0 100644 --- a/packages/git/src/browser/dirty-diff/dirty-diff-manager.ts +++ b/packages/git/src/browser/dirty-diff/dirty-diff-manager.ts @@ -71,13 +71,13 @@ export class DirtyDiffManager { protected async handleEditorCreated(editorWidget: EditorWidget): Promise { const editor = editorWidget.editor; - const uri = editor.uri.toString(); - if (editor.uri.scheme !== 'file') { + if (!this.supportsDirtyDiff(editor)) { return; } const toDispose = new DisposableCollection(); const model = this.createNewModel(editor); toDispose.push(model); + const uri = editor.uri.toString(); this.models.set(uri, model); toDispose.push(editor.onDocumentContentChanged(throttle((event: TextDocumentChangeEvent) => model.handleDocumentChanged(event.document), 1000))); editorWidget.disposed.connect(() => { @@ -93,6 +93,10 @@ export class DirtyDiffManager { model.handleDocumentChanged(editor.document); } + protected supportsDirtyDiff(editor: TextEditor): boolean { + return editor.uri.scheme === 'file' && editor.shouldDisplayDirtyDiff(); + } + protected createNewModel(editor: TextEditor): DirtyDiffModel { const previousRevision = this.createPreviousFileRevision(editor.uri); const model = new DirtyDiffModel(editor, this.preferences, previousRevision); @@ -101,11 +105,14 @@ export class DirtyDiffManager { } protected createPreviousFileRevision(fileUri: URI): DirtyDiffModel.PreviousFileRevision { + const getOriginalUri = (staged: boolean): URI => { + const query = staged ? '' : 'HEAD'; + return fileUri.withScheme(GIT_RESOURCE_SCHEME).withQuery(query); + }; return { fileUri, getContents: async (staged: boolean) => { - const query = staged ? '' : 'HEAD'; - const uri = fileUri.withScheme(GIT_RESOURCE_SCHEME).withQuery(query); + const uri = getOriginalUri(staged); const gitResource = await this.gitResourceResolver.getResource(uri); return gitResource.readContents(); }, @@ -115,7 +122,8 @@ export class DirtyDiffManager { return this.git.lsFiles(repository, fileUri.toString(), { errorUnmatch: true }); } return false; - } + }, + getOriginalUri }; } @@ -128,7 +136,6 @@ export class DirtyDiffManager { await model.handleGitStatusUpdate(repository, changes); } } - } export class DirtyDiffModel implements Disposable { @@ -137,7 +144,7 @@ export class DirtyDiffModel implements Disposable { protected enabled = true; protected staged: boolean; - protected previousContent: ContentLines | undefined; + protected previousContent: DirtyDiffModel.PreviousRevisionContent | undefined; protected currentContent: ContentLines | undefined; protected readonly onDirtyDiffUpdateEmitter = new Emitter(); @@ -181,7 +188,7 @@ export class DirtyDiffModel implements Disposable { update(): void { const editor = this.editor; if (!this.shouldRender()) { - this.onDirtyDiffUpdateEmitter.fire({ editor, added: [], removed: [], modified: [] }); + this.onDirtyDiffUpdateEmitter.fire({ editor, changes: [] }); return; } if (this.updateTimeout) { @@ -200,7 +207,7 @@ export class DirtyDiffModel implements Disposable { // a new update task should be scheduled anyway. return; } - const dirtyDiffUpdate = { editor, ...dirtyDiff }; + const dirtyDiffUpdate = { editor, previousRevisionUri: previous.uri, ...dirtyDiff }; this.onDirtyDiffUpdateEmitter.fire(dirtyDiffUpdate); }, 100); } @@ -251,9 +258,13 @@ export class DirtyDiffModel implements Disposable { return modelUri.startsWith(repoUri) && this.previousRevision.isVersionControlled(); } - protected async getPreviousRevisionContent(): Promise { - const contents = await this.previousRevision.getContents(this.staged); - return contents ? ContentLines.fromString(contents) : undefined; + protected async getPreviousRevisionContent(): Promise { + const { previousRevision, staged } = this; + const contents = await previousRevision.getContents(staged); + if (contents) { + const uri = previousRevision.getOriginalUri?.(staged); + return { ...ContentLines.fromString(contents), uri }; + } } dispose(): void { @@ -282,16 +293,18 @@ export namespace DirtyDiffModel { } export function documentContentLines(document: TextEditorDocument): ContentLines { - return { - length: document.lineCount, - getLineContent: line => document.getLineContent(line + 1), - }; + return ContentLines.fromTextEditorDocument(document); } export interface PreviousFileRevision { readonly fileUri: URI; getContents(staged: boolean): Promise; isVersionControlled(): Promise; + getOriginalUri?(staged: boolean): URI; + } + + export interface PreviousRevisionContent extends ContentLines { + readonly uri?: URI; } } diff --git a/packages/git/src/browser/git-contribution.ts b/packages/git/src/browser/git-contribution.ts index 8232ecf989c0d..042f2f84cdf5e 100644 --- a/packages/git/src/browser/git-contribution.ts +++ b/packages/git/src/browser/git-contribution.ts @@ -23,16 +23,17 @@ import { MenuAction, MenuContribution, MenuModelRegistry, + MessageService, Mutable } from '@theia/core'; -import { codicon, DiffUris, Widget } from '@theia/core/lib/browser'; +import { codicon, DiffUris, Widget, open, OpenerService } from '@theia/core/lib/browser'; import { TabBarToolbarContribution, TabBarToolbarItem, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { EditorContextMenu, EditorManager, EditorOpenerOptions, EditorWidget } from '@theia/editor/lib/browser'; -import { Git, GitFileChange, GitFileStatus } from '../common'; +import { Git, GitFileChange, GitFileStatus, GitWatcher, Repository } from '../common'; import { GitRepositoryTracker } from './git-repository-tracker'; import { GitAction, GitQuickOpenService } from './git-quick-open-service'; import { GitSyncService } from './git-sync-service'; @@ -42,6 +43,8 @@ import { GitErrorHandler } from '../browser/git-error-handler'; import { ScmWidget } from '@theia/scm/lib/browser/scm-widget'; import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget'; import { ScmCommand, ScmResource } from '@theia/scm/lib/browser/scm-provider'; +import { LineRange } from '@theia/scm/lib/browser/dirty-diff/diff-computer'; +import { DirtyDiffWidget, SCM_CHANGE_TITLE_MENU } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-widget'; import { ProgressService } from '@theia/core/lib/common/progress-service'; import { GitPreferences } from './git-preferences'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; @@ -166,6 +169,18 @@ export namespace GIT_COMMANDS { label: 'Stage All Changes', iconClass: codicon('add') }, 'vscode.git/package/command.stageAll', GIT_CATEGORY_KEY); + export const STAGE_CHANGE = Command.toLocalizedCommand({ + id: 'git.stage.change', + category: GIT_CATEGORY, + label: 'Stage Change', + iconClass: codicon('add') + }, 'vscode.git/package/command.stageChange', GIT_CATEGORY_KEY); + export const REVERT_CHANGE = Command.toLocalizedCommand({ + id: 'git.revert.change', + category: GIT_CATEGORY, + label: 'Revert Change', + iconClass: codicon('discard') + }, 'vscode.git/package/command.revertChange', GIT_CATEGORY_KEY); export const UNSTAGE = Command.toLocalizedCommand({ id: 'git.unstage', category: GIT_CATEGORY, @@ -267,6 +282,8 @@ export class GitContribution implements CommandContribution, MenuContribution, T protected toDispose = new DisposableCollection(); + @inject(OpenerService) protected openerService: OpenerService; + @inject(MessageService) protected messageService: MessageService; @inject(EditorManager) protected readonly editorManager: EditorManager; @inject(GitQuickOpenService) protected readonly quickOpenService: GitQuickOpenService; @inject(GitRepositoryTracker) protected readonly repositoryTracker: GitRepositoryTracker; @@ -280,6 +297,7 @@ export class GitContribution implements CommandContribution, MenuContribution, T @inject(GitPreferences) protected readonly gitPreferences: GitPreferences; @inject(DecorationsService) protected readonly decorationsService: DecorationsService; @inject(GitDecorationProvider) protected readonly gitDecorationProvider: GitDecorationProvider; + @inject(GitWatcher) protected readonly gitWatcher: GitWatcher; onStart(): void { this.updateStatusBar(); @@ -385,6 +403,15 @@ export class GitContribution implements CommandContribution, MenuContribution, T commandId: GIT_COMMANDS.DISCARD_ALL.id, when: 'scmProvider == git && scmResourceGroup == workingTree || scmProvider == git && scmResourceGroup == untrackedChanges', }); + + menus.registerMenuAction(SCM_CHANGE_TITLE_MENU, { + commandId: GIT_COMMANDS.STAGE_CHANGE.id, + when: 'scmProvider == git' + }); + menus.registerMenuAction(SCM_CHANGE_TITLE_MENU, { + commandId: GIT_COMMANDS.REVERT_CHANGE.id, + when: 'scmProvider == git' + }); } registerCommands(registry: CommandRegistry): void { @@ -538,7 +565,9 @@ export class GitContribution implements CommandContribution, MenuContribution, T registry.registerCommand(GIT_COMMANDS.OPEN_CHANGED_FILE, { execute: (...arg: ScmResource[]) => { for (const resource of arg) { - this.editorManager.open(resource.sourceUri, { mode: 'reveal' }); + open(this.openerService, resource.sourceUri, { mode: 'reveal' }).catch(e => { + this.messageService.error(e.message); + }); } } }); @@ -573,6 +602,14 @@ export class GitContribution implements CommandContribution, MenuContribution, T isEnabled: widget => this.workspaceService.opened && (!widget || widget instanceof ScmWidget) && !this.repositoryProvider.selectedRepository, isVisible: widget => this.workspaceService.opened && (!widget || widget instanceof ScmWidget) && !this.repositoryProvider.selectedRepository }); + registry.registerCommand(GIT_COMMANDS.STAGE_CHANGE, { + execute: (widget: DirtyDiffWidget) => this.withProgress(() => this.stageChange(widget)), + isEnabled: widget => widget instanceof DirtyDiffWidget + }); + registry.registerCommand(GIT_COMMANDS.REVERT_CHANGE, { + execute: (widget: DirtyDiffWidget) => this.withProgress(() => this.revertChange(widget)), + isEnabled: widget => widget instanceof DirtyDiffWidget + }); } async amend(): Promise { { @@ -622,7 +659,7 @@ export class GitContribution implements CommandContribution, MenuContribution, T tooltip: GIT_COMMANDS.INIT_REPOSITORY.label }); - const registerItem = (item: Mutable) => { + const registerItem = (item: Mutable) => { const commandId = item.command; const id = '__git.tabbar.toolbar.' + commandId; const command = this.commands.getCommand(commandId); @@ -922,6 +959,62 @@ export class GitContribution implements CommandContribution, MenuContribution, T } + async stageChange(widget: DirtyDiffWidget): Promise { + const scmRepository = this.repositoryProvider.selectedScmRepository; + if (!scmRepository) { + return; + } + + const repository = scmRepository.provider.repository; + + const path = Repository.relativePath(repository, widget.uri)?.toString(); + if (!path) { + return; + } + + const { currentChange } = widget; + if (!currentChange) { + return; + } + + const dataToStage = await widget.getContentWithSelectedChanges(change => change === currentChange); + + try { + const hash = (await this.git.exec(repository, ['hash-object', '--stdin', '-w', '--path', path], { stdin: dataToStage, stdinEncoding: 'utf8' })).stdout.trim(); + + let mode = (await this.git.exec(repository, ['ls-files', '--format=%(objectmode)', '--', path])).stdout.split('\n').filter(line => !!line.trim())[0]; + if (!mode) { + mode = '100644'; // regular non-executable file + } + + await this.git.exec(repository, ['update-index', '--add', '--cacheinfo', mode, hash, path]); + + // enforce a notification as there would be no status update if the file had been staged already + this.gitWatcher.onGitChanged({ source: repository, status: await this.git.status(repository) }); + } catch (error) { + this.gitErrorHandler.handleError(error); + } + + widget.editor.cursor = LineRange.getStartPosition(currentChange.currentRange); + } + + async revertChange(widget: DirtyDiffWidget): Promise { + const { currentChange } = widget; + if (!currentChange) { + return; + } + + const editor = widget.editor.getControl(); + editor.pushUndoStop(); + editor.executeEdits('Revert Change', [{ + range: editor.getModel()!.getFullModelRange(), + text: await widget.getContentWithSelectedChanges(change => change !== currentChange) + }]); + editor.pushUndoStop(); + + widget.editor.cursor = LineRange.getStartPosition(currentChange.currentRange); + } + /** * It should be aligned with https://code.visualstudio.com/api/references/theme-color#git-colors */ diff --git a/packages/git/src/browser/git-file-service-contribution.ts b/packages/git/src/browser/git-file-service-contribution.ts new file mode 100644 index 0000000000000..7719eca95a4c7 --- /dev/null +++ b/packages/git/src/browser/git-file-service-contribution.ts @@ -0,0 +1,33 @@ +// ***************************************************************************** +// Copyright (C) 2024 1C-Soft LLC 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 { interfaces } from '@theia/core/shared/inversify'; +import { FileService, FileServiceContribution } from '@theia/filesystem/lib/browser/file-service'; +import { GitFileSystemProvider } from './git-file-system-provider'; +import { GIT_RESOURCE_SCHEME } from './git-resource'; + +export class GitFileServiceContribution implements FileServiceContribution { + + constructor(protected readonly container: interfaces.Container) { } + + registerFileSystemProviders(service: FileService): void { + service.onWillActivateFileSystemProvider(event => { + if (event.scheme === GIT_RESOURCE_SCHEME) { + service.registerProvider(GIT_RESOURCE_SCHEME, this.container.get(GitFileSystemProvider)); + } + }); + } +} diff --git a/packages/git/src/browser/git-file-system-provider.ts b/packages/git/src/browser/git-file-system-provider.ts new file mode 100644 index 0000000000000..113aeee6d16c7 --- /dev/null +++ b/packages/git/src/browser/git-file-system-provider.ts @@ -0,0 +1,84 @@ +// ***************************************************************************** +// Copyright (C) 2024 1C-Soft LLC 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 { Event, URI, Disposable } from '@theia/core'; +import { + FileChange, + FileDeleteOptions, + FileOverwriteOptions, + FileSystemProvider, + FileSystemProviderCapabilities, + FileType, + FileWriteOptions, + Stat, + WatchOptions +} from '@theia/filesystem/lib/common/files'; +import { GitResourceResolver } from './git-resource-resolver'; +import { EncodingService } from '@theia/core/lib/common/encoding-service'; + +@injectable() +export class GitFileSystemProvider implements FileSystemProvider { + + readonly capabilities = FileSystemProviderCapabilities.Readonly | + FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive; + + readonly onDidChangeCapabilities: Event = Event.None; + readonly onDidChangeFile: Event = Event.None; + readonly onFileWatchError: Event = Event.None; + + @inject(GitResourceResolver) + protected readonly resourceResolver: GitResourceResolver; + + @inject(EncodingService) + protected readonly encodingService: EncodingService; + + watch(resource: URI, opts: WatchOptions): Disposable { + return Disposable.NULL; + } + + async stat(resource: URI): Promise { + const gitResource = await this.resourceResolver.getResource(resource); + const size = await gitResource.getSize(); + return { type: FileType.File, mtime: 0, ctime: 0, size }; + } + + async readFile(resource: URI): Promise { + const gitResource = await this.resourceResolver.getResource(resource); + const contents = await gitResource.readContents({ encoding: 'binary' }); + return this.encodingService.encode(contents, { encoding: 'binary', hasBOM: false }).buffer; + } + + writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + throw new Error('Method not implemented.'); + } + + mkdir(resource: URI): Promise { + throw new Error('Method not implemented.'); + } + + readdir(resource: URI): Promise<[string, FileType][]> { + throw new Error('Method not implemented.'); + } + + delete(resource: URI, opts: FileDeleteOptions): Promise { + throw new Error('Method not implemented.'); + } + + rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/git/src/browser/git-frontend-module.ts b/packages/git/src/browser/git-frontend-module.ts index 132f15aa2b745..3c10f70a8b732 100644 --- a/packages/git/src/browser/git-frontend-module.ts +++ b/packages/git/src/browser/git-frontend-module.ts @@ -43,6 +43,9 @@ import { ScmHistorySupport } from '@theia/scm-extra/lib/browser/history/scm-hist import { ScmHistoryProvider } from '@theia/scm-extra/lib/browser/history'; import { GitHistorySupport } from './history/git-history-support'; import { GitDecorationProvider } from './git-decoration-provider'; +import { GitFileSystemProvider } from './git-file-system-provider'; +import { GitFileServiceContribution } from './git-file-service-contribution'; +import { FileServiceContribution } from '@theia/filesystem/lib/browser/file-service'; export default new ContainerModule(bind => { bindGitPreferences(bind); @@ -75,6 +78,10 @@ export default new ContainerModule(bind => { bind(GitSyncService).toSelf().inSingletonScope(); bind(GitErrorHandler).toSelf().inSingletonScope(); + + bind(GitFileSystemProvider).toSelf().inSingletonScope(); + bind(GitFileServiceContribution).toDynamicValue(ctx => new GitFileServiceContribution(ctx.container)).inSingletonScope(); + bind(FileServiceContribution).toService(GitFileServiceContribution); }); export function createGitScmProviderFactory(ctx: interfaces.Context): GitScmProvider.Factory { diff --git a/packages/git/src/browser/git-quick-open-service.ts b/packages/git/src/browser/git-quick-open-service.ts index 965037e1dbed6..cc83fe0711079 100644 --- a/packages/git/src/browser/git-quick-open-service.ts +++ b/packages/git/src/browser/git-quick-open-service.ts @@ -364,7 +364,7 @@ export class GitQuickOpenService { const getItems = (lookFor?: string) => { const items = []; if (!lookFor) { - const label = nls.localize('theia/git/amendReuseMessag', "To reuse the last commit message, press 'Enter' or 'Escape' to cancel."); + const label = nls.localize('theia/git/amendReuseMessage', "To reuse the last commit message, press 'Enter' or 'Escape' to cancel."); items.push(new GitQuickPickItem(label, () => resolve(lastMessage), label)); } else { items.push(new GitQuickPickItem( diff --git a/packages/git/src/browser/git-repository-provider.spec.ts b/packages/git/src/browser/git-repository-provider.spec.ts index 01bd5ba328b08..3c940636e6a19 100644 --- a/packages/git/src/browser/git-repository-provider.spec.ts +++ b/packages/git/src/browser/git-repository-provider.spec.ts @@ -26,7 +26,7 @@ import { DugiteGit } from '../node/dugite-git'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { FileStat, FileChangesEvent } from '@theia/filesystem/lib/common/files'; import { Emitter, CommandService, Disposable } from '@theia/core'; -import { LocalStorageService, StorageService, LabelProvider } from '@theia/core/lib/browser'; +import { LocalStorageService, StorageService, LabelProvider, OpenerService } from '@theia/core/lib/browser'; import { GitRepositoryProvider } from './git-repository-provider'; import * as sinon from 'sinon'; import * as chai from 'chai'; @@ -97,6 +97,7 @@ describe('GitRepositoryProvider', () => { testContainer.bind(ScmContextKeyService).toSelf().inSingletonScope(); testContainer.bind(ContextKeyService).to(ContextKeyServiceDummyImpl).inSingletonScope(); testContainer.bind(GitCommitMessageValidator).toSelf().inSingletonScope(); + testContainer.bind(OpenerService).toConstantValue({}); testContainer.bind(EditorManager).toConstantValue({}); testContainer.bind(GitErrorHandler).toConstantValue({}); testContainer.bind(CommandService).toConstantValue({}); diff --git a/packages/git/src/browser/git-resource.ts b/packages/git/src/browser/git-resource.ts index 7389da945c13a..3d74f3dfb28d7 100644 --- a/packages/git/src/browser/git-resource.ts +++ b/packages/git/src/browser/git-resource.ts @@ -36,5 +36,20 @@ export class GitResource implements Resource { return ''; } + async getSize(): Promise { + if (this.repository) { + const path = Repository.relativePath(this.repository, this.uri.withScheme('file'))?.toString(); + if (path) { + const commitish = this.uri.query || 'index'; + const args = commitish !== 'index' ? ['ls-tree', '--format=%(objectsize)', commitish, path] : ['ls-files', '--format=%(objectsize)', '--', path]; + const size = (await this.git.exec(this.repository, args)).stdout.split('\n').filter(line => !!line.trim())[0]; + if (size) { + return parseInt(size); + } + } + } + return 0; + } + dispose(): void { } } diff --git a/packages/git/src/browser/git-scm-provider.spec.ts b/packages/git/src/browser/git-scm-provider.spec.ts index 4458af4af177a..a3b3cfb51a905 100644 --- a/packages/git/src/browser/git-scm-provider.spec.ts +++ b/packages/git/src/browser/git-scm-provider.spec.ts @@ -21,7 +21,7 @@ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/front FrontendApplicationConfigProvider.set({}); import { CommandService, Disposable, ILogger, MessageService } from '@theia/core'; -import { LabelProvider } from '@theia/core/lib/browser'; +import { LabelProvider, OpenerService } from '@theia/core/lib/browser'; import { FileUri } from '@theia/core/lib/node'; import { Container } from '@theia/core/shared/inversify'; import { EditorManager } from '@theia/editor/lib/browser'; @@ -31,7 +31,7 @@ import { expect } from 'chai'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; -import * as rimraf from 'rimraf'; +import { rimraf } from 'rimraf'; import * as sinon from 'sinon'; import { Git, GitFileStatus, Repository } from '../common'; import { DugiteGit } from '../node/dugite-git'; @@ -46,6 +46,7 @@ disableJSDOM(); describe('GitScmProvider', () => { let testContainer: Container; + let mockOpenerService: OpenerService; let mockEditorManager: EditorManager; let mockGitErrorHandler: GitErrorHandler; let mockFileService: FileService; @@ -65,6 +66,7 @@ describe('GitScmProvider', () => { }); beforeEach(async () => { + mockOpenerService = {} as OpenerService; mockEditorManager = sinon.createStubInstance(EditorManager); mockGitErrorHandler = sinon.createStubInstance(GitErrorHandler); mockFileService = sinon.createStubInstance(FileService); @@ -73,6 +75,7 @@ describe('GitScmProvider', () => { mockLabelProvider = sinon.createStubInstance(LabelProvider); testContainer = new Container(); + testContainer.bind(OpenerService).toConstantValue(mockOpenerService); testContainer.bind(EditorManager).toConstantValue(mockEditorManager); testContainer.bind(GitErrorHandler).toConstantValue(mockGitErrorHandler); testContainer.bind(FileService).toConstantValue(mockFileService); @@ -106,12 +109,7 @@ describe('GitScmProvider', () => { }); afterEach(async () => { - await new Promise((resolve, reject) => rimraf(FileUri.fsPath(repository.localUri), error => { - if (error) { - reject(error); - } - resolve(); - })); + await rimraf(FileUri.fsPath(repository.localUri)); }); it('should unstage all the changes', async () => { diff --git a/packages/git/src/browser/git-scm-provider.ts b/packages/git/src/browser/git-scm-provider.ts index 0eb2d33c95096..c2b51109c6b70 100644 --- a/packages/git/src/browser/git-scm-provider.ts +++ b/packages/git/src/browser/git-scm-provider.ts @@ -16,6 +16,7 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; +import { open, OpenerService } from '@theia/core/lib/browser'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; import { Emitter } from '@theia/core'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; @@ -64,6 +65,9 @@ export class GitScmProvider implements ScmProvider { this.onDidChangeStatusBarCommandsEmitter ); + @inject(OpenerService) + protected openerService: OpenerService; + @inject(EditorManager) protected readonly editorManager: EditorManager; @@ -223,9 +227,12 @@ export class GitScmProvider implements ScmProvider { async open(change: GitFileChange, options?: EditorOpenerOptions): Promise { const uriToOpen = this.getUriToOpen(change); - await this.editorManager.open(uriToOpen, options); + await open(this.openerService, uriToOpen, options); } + // note: the implementation has to ensure that `GIT_RESOURCE_SCHEME` URIs it returns either directly or within a diff-URI always have a query; + // as an example of an issue that can otherwise arise, the VS Code `media-preview` plugin is known to mangle resource URIs without the query: + // https://github.com/microsoft/vscode/blob/6eaf6487a4d8301b981036bfa53976546eb6694f/extensions/media-preview/src/imagePreview/index.ts#L205-L209 getUriToOpen(change: GitFileChange): URI { const changeUri: URI = new URI(change.uri); const fromFileUri = change.oldUri ? new URI(change.oldUri) : changeUri; // set oldUri on renamed and copied @@ -233,14 +240,14 @@ export class GitScmProvider implements ScmProvider { if (change.staged) { return changeUri.withScheme(GIT_RESOURCE_SCHEME).withQuery('HEAD'); } else { - return changeUri.withScheme(GIT_RESOURCE_SCHEME); + return changeUri.withScheme(GIT_RESOURCE_SCHEME).withQuery('index'); } } if (change.status !== GitFileStatus.New) { if (change.staged) { return DiffUris.encode( fromFileUri.withScheme(GIT_RESOURCE_SCHEME).withQuery('HEAD'), - changeUri.withScheme(GIT_RESOURCE_SCHEME), + changeUri.withScheme(GIT_RESOURCE_SCHEME).withQuery('index'), nls.localize( 'theia/git/tabTitleIndex', '{0} (Index)', @@ -249,7 +256,7 @@ export class GitScmProvider implements ScmProvider { } if (this.stagedChanges.find(c => c.uri === change.uri)) { return DiffUris.encode( - fromFileUri.withScheme(GIT_RESOURCE_SCHEME), + fromFileUri.withScheme(GIT_RESOURCE_SCHEME).withQuery('index'), changeUri, nls.localize( 'theia/git/tabTitleWorkingTree', @@ -270,11 +277,11 @@ export class GitScmProvider implements ScmProvider { )); } if (change.staged) { - return changeUri.withScheme(GIT_RESOURCE_SCHEME); + return changeUri.withScheme(GIT_RESOURCE_SCHEME).withQuery('index'); } if (this.stagedChanges.find(c => c.uri === change.uri)) { return DiffUris.encode( - changeUri.withScheme(GIT_RESOURCE_SCHEME), + changeUri.withScheme(GIT_RESOURCE_SCHEME).withQuery('index'), changeUri, nls.localize( 'theia/git/tabTitleWorkingTree', diff --git a/packages/git/src/node/dugite-git-watcher.slow-spec.ts b/packages/git/src/node/dugite-git-watcher.slow-spec.ts index 5a4f0e95bdd4f..843e092029471 100644 --- a/packages/git/src/node/dugite-git-watcher.slow-spec.ts +++ b/packages/git/src/node/dugite-git-watcher.slow-spec.ts @@ -18,7 +18,7 @@ import * as fs from '@theia/core/shared/fs-extra'; import * as temp from 'temp'; import * as path from 'path'; import { expect } from 'chai'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { Git } from '../common/git'; import { DugiteGit } from './dugite-git'; import { Repository } from '../common'; diff --git a/packages/git/src/node/dugite-git.slow-spec.ts b/packages/git/src/node/dugite-git.slow-spec.ts index 888507b401d54..b4954dd321188 100644 --- a/packages/git/src/node/dugite-git.slow-spec.ts +++ b/packages/git/src/node/dugite-git.slow-spec.ts @@ -16,7 +16,7 @@ import * as temp from 'temp'; import { expect } from 'chai'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { GitFileStatus } from '../common'; import { createGit } from './test/binding-helper'; diff --git a/packages/git/src/node/dugite-git.spec.ts b/packages/git/src/node/dugite-git.spec.ts index db3592f321c7c..340fafaf6e1b2 100644 --- a/packages/git/src/node/dugite-git.spec.ts +++ b/packages/git/src/node/dugite-git.spec.ts @@ -22,7 +22,7 @@ import * as fs from '@theia/core/shared/fs-extra'; import { expect } from 'chai'; import { Git } from '../common/git'; import { git as gitExec } from 'dugite-extra/lib/core/git'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { WorkingDirectoryStatus, Repository, GitUtils, GitFileStatus, GitFileChange } from '../common'; import { initRepository, createTestRepository } from 'dugite-extra/lib/command/test-helper'; import { createGit } from './test/binding-helper'; diff --git a/packages/git/src/node/dugite-git.ts b/packages/git/src/node/dugite-git.ts index e72ad9dee1724..8fe73bc453039 100644 --- a/packages/git/src/node/dugite-git.ts +++ b/packages/git/src/node/dugite-git.ts @@ -24,7 +24,7 @@ import { clone } from 'dugite-extra/lib/command/clone'; import { fetch } from 'dugite-extra/lib/command/fetch'; import { stash } from 'dugite-extra/lib/command/stash'; import { merge } from 'dugite-extra/lib/command/merge'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { getStatus } from 'dugite-extra/lib/command/status'; import { createCommit } from 'dugite-extra/lib/command/commit'; import { stage, unstage } from 'dugite-extra/lib/command/stage'; @@ -48,6 +48,8 @@ import { GitExecProvider } from './git-exec-provider'; import { GitEnvProvider } from './env/git-env-provider'; import { GitInit } from './init/git-init'; +import upath = require('upath'); + /** * Parsing and converting raw Git output into Git model instances. */ @@ -548,7 +550,9 @@ export class DugiteGit implements Git { const path = this.getFsPath(uri); const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]); if (encoding === 'binary') { - return (await getBlobContents(repositoryPath, commitish, path, { exec, env })).toString(); + // note: contrary to what its jsdoc says, getBlobContents expects a (normalized) relative path + const relativePath = upath.normalizeSafe(Path.relative(repositoryPath, path)); + return (await getBlobContents(repositoryPath, commitish, relativePath, { exec, env })).toString('binary'); } return (await getTextContents(repositoryPath, commitish, path, { exec, env })).toString(); } diff --git a/packages/git/src/node/git-locator/git-locator-host.ts b/packages/git/src/node/git-locator/git-locator-host.ts index 1dac54d47a8d6..ac4de50adcfc9 100644 --- a/packages/git/src/node/git-locator/git-locator-host.ts +++ b/packages/git/src/node/git-locator/git-locator-host.ts @@ -14,6 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import '@theia/core/shared/reflect-metadata'; import { RpcProxyFactory } from '@theia/core'; import { IPCEntryPoint } from '@theia/core/lib/node/messaging/ipc-protocol'; import { GitLocatorImpl } from './git-locator-impl'; diff --git a/packages/git/src/node/git-repository-watcher.ts b/packages/git/src/node/git-repository-watcher.ts index 3e1bc8a43e8b1..455842b8974b3 100644 --- a/packages/git/src/node/git-repository-watcher.ts +++ b/packages/git/src/node/git-repository-watcher.ts @@ -96,9 +96,11 @@ export class GitRepositoryWatcher implements Disposable { } else { const idleTimeout = this.watching ? 5000 : /* super long */ 1000 * 60 * 60 * 24; await new Promise(resolve => { + this.idle = true; const id = setTimeout(resolve, idleTimeout); this.interruptIdle = () => { clearTimeout(id); resolve(); }; }).then(() => { + this.idle = false; this.interruptIdle = undefined; }); } diff --git a/packages/keymaps/package.json b/packages/keymaps/package.json index 16728e9341d9c..5a0bbff683274 100644 --- a/packages/keymaps/package.json +++ b/packages/keymaps/package.json @@ -1,17 +1,18 @@ { "name": "@theia/keymaps", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Custom Keymaps Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/monaco": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/preferences": "1.44.0", - "@theia/userstorage": "1.44.0", - "jsonc-parser": "^2.2.0" + "@theia/core": "1.54.0", + "@theia/monaco": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", + "@theia/preferences": "1.54.0", + "@theia/userstorage": "1.54.0", + "jsonc-parser": "^2.2.0", + "tslib": "^2.6.2" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "publishConfig": { "access": "public" diff --git a/packages/keymaps/src/browser/keymaps-frontend-module.ts b/packages/keymaps/src/browser/keymaps-frontend-module.ts index 2056444246231..5a2c1476bc357 100644 --- a/packages/keymaps/src/browser/keymaps-frontend-module.ts +++ b/packages/keymaps/src/browser/keymaps-frontend-module.ts @@ -22,7 +22,7 @@ import { KeymapsFrontendContribution } from './keymaps-frontend-contribution'; import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; import { KeybindingContribution } from '@theia/core/lib/browser/keybinding'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { WidgetFactory } from '@theia/core/lib/browser'; +import { noopWidgetStatusBarContribution, WidgetFactory, WidgetStatusBarContribution } from '@theia/core/lib/browser'; import { KeybindingWidget } from './keybindings-widget'; import { KeybindingSchemaUpdater } from './keybinding-schema-updater'; import { JsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store'; @@ -41,4 +41,5 @@ export default new ContainerModule(bind => { })).inSingletonScope(); bind(KeybindingSchemaUpdater).toSelf().inSingletonScope(); bind(JsonSchemaContribution).toService(KeybindingSchemaUpdater); + bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(KeybindingWidget)); }); diff --git a/packages/markers/package.json b/packages/markers/package.json index 11ad5167e0b0b..47aba430ee260 100644 --- a/packages/markers/package.json +++ b/packages/markers/package.json @@ -1,11 +1,12 @@ { "name": "@theia/markers", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Markers Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/workspace": "1.44.0" + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/workspace": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -40,7 +41,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/markers/src/browser/problem/problem-composite-tree-node.ts b/packages/markers/src/browser/problem/problem-composite-tree-node.ts index a5b20be14516b..5cdd426d05bde 100644 --- a/packages/markers/src/browser/problem/problem-composite-tree-node.ts +++ b/packages/markers/src/browser/problem/problem-composite-tree-node.ts @@ -23,6 +23,11 @@ import { ProblemUtils } from './problem-utils'; export namespace ProblemCompositeTreeNode { + export interface Child { + node: MarkerInfoNode; + markers: Marker[]; + } + export function setSeverity(parent: MarkerInfoNode, markers: Marker[]): void { let maxSeverity: DiagnosticSeverity | undefined; markers.forEach(marker => { @@ -33,7 +38,7 @@ export namespace ProblemCompositeTreeNode { parent.severity = maxSeverity; }; - export function addChildren(parent: CompositeTreeNode, insertChildren: { node: MarkerInfoNode, markers: Marker[] }[]): void { + export function addChildren(parent: CompositeTreeNode, insertChildren: ProblemCompositeTreeNode.Child[]): void { for (const { node, markers } of insertChildren) { ProblemCompositeTreeNode.setSeverity(node, markers); } diff --git a/packages/markers/src/browser/problem/problem-preferences.ts b/packages/markers/src/browser/problem/problem-preferences.ts index b778d514ed9e4..66ca217bff6f4 100644 --- a/packages/markers/src/browser/problem/problem-preferences.ts +++ b/packages/markers/src/browser/problem/problem-preferences.ts @@ -23,7 +23,7 @@ export const ProblemConfigSchema: PreferenceSchema = { 'properties': { 'problems.decorations.enabled': { 'type': 'boolean', - 'description': nls.localizeByDefault('Show Errors & Warnings on files and folder.'), + 'description': nls.localizeByDefault('Show Errors & Warnings on files and folder. Overwritten by `#problems.visibility#` when it is off.'), 'default': true, }, 'problems.decorations.tabbar.enabled': { diff --git a/packages/markers/src/browser/problem/problem-tree-model.ts b/packages/markers/src/browser/problem/problem-tree-model.ts index 152feb7a33408..d4eab7184bd83 100644 --- a/packages/markers/src/browser/problem/problem-tree-model.ts +++ b/packages/markers/src/browser/problem/problem-tree-model.ts @@ -28,7 +28,8 @@ import debounce = require('@theia/core/shared/lodash.debounce'); @injectable() export class ProblemTree extends MarkerTree { - protected markers: { node: MarkerInfoNode, markers: Marker[] }[] = []; + + protected queuedMarkers = new Map(); constructor( @inject(ProblemManager) markerManager: ProblemManager, @@ -79,20 +80,28 @@ export class ProblemTree extends MarkerTree { } protected override insertNodeWithMarkers(node: MarkerInfoNode, markers: Marker[]): void { - this.markers.push({ node, markers }); + // Add the element to the queue. + // In case a diagnostics collection for the same file already exists, it will be replaced. + this.queuedMarkers.set(node.id, { node, markers }); this.doInsertNodesWithMarkers(); } protected doInsertNodesWithMarkers = debounce(() => { - ProblemCompositeTreeNode.addChildren(this.root as MarkerRootNode, this.markers); + const root = this.root; + // Sanity check; This should always be of type `MarkerRootNode` + if (!MarkerRootNode.is(root)) { + return; + } + const queuedItems = Array.from(this.queuedMarkers.values()); + ProblemCompositeTreeNode.addChildren(root, queuedItems); - for (const { node, markers } of this.markers) { + for (const { node, markers } of queuedItems) { const children = this.getMarkerNodes(node, markers); node.numberOfMarkers = markers.length; this.setChildren(node, children); } - this.markers.length = 0; + this.queuedMarkers.clear(); }, 50); } diff --git a/packages/memory-inspector/package.json b/packages/memory-inspector/package.json index d78b4049cbd5d..a505a90f24873 100644 --- a/packages/memory-inspector/package.json +++ b/packages/memory-inspector/package.json @@ -1,6 +1,6 @@ { "name": "@theia/memory-inspector", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Memory Inspector", "keywords": [ "theia-extension" @@ -27,10 +27,11 @@ "watch": "theiaext watch" }, "dependencies": { - "@theia/core": "1.44.0", - "@theia/debug": "1.44.0", + "@theia/core": "1.54.0", + "@theia/debug": "1.54.0", "@vscode/debugprotocol": "^1.51.0", - "long": "^4.0.0" + "long": "^4.0.0", + "tslib": "^2.6.2" }, "devDependencies": { "@types/long": "^4.0.0" diff --git a/packages/messages/package.json b/packages/messages/package.json index a66892583fcbd..d0526320a5100 100644 --- a/packages/messages/package.json +++ b/packages/messages/package.json @@ -1,11 +1,12 @@ { "name": "@theia/messages", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Messages Extension", "dependencies": { - "@theia/core": "1.44.0", + "@theia/core": "1.54.0", "react-perfect-scrollbar": "^1.5.3", - "ts-md5": "^1.2.2" + "ts-md5": "^1.2.2", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -40,7 +41,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/messages/src/browser/notifications-manager.ts b/packages/messages/src/browser/notifications-manager.ts index c074720d7fab2..fcec7704e827d 100644 --- a/packages/messages/src/browser/notifications-manager.ts +++ b/packages/messages/src/browser/notifications-manager.ts @@ -180,18 +180,23 @@ export class NotificationManager extends MessageClient { override showMessage(plainMessage: PlainMessage): Promise { const messageId = this.getMessageId(plainMessage); - let notification = this.notifications.get(messageId); - if (!notification) { - const message = this.contentRenderer.renderMessage(plainMessage.text); - const type = this.toNotificationType(plainMessage.type); - const actions = Array.from(new Set(plainMessage.actions)); - const source = plainMessage.source; - const expandable = this.isExpandable(message, source, actions); - const collapsed = expandable; - notification = { messageId, message, type, actions, expandable, collapsed }; - this.notifications.set(messageId, notification); + this.toasts.delete(messageId); + this.notifications.delete(messageId); + const existingDeferred = this.deferredResults.get(messageId); + if (existingDeferred) { + this.deferredResults.delete(messageId); + existingDeferred.resolve(undefined); } - const result = this.deferredResults.get(messageId) || new Deferred(); + + const message = this.contentRenderer.renderMessage(plainMessage.text); + const type = this.toNotificationType(plainMessage.type); + const actions = Array.from(new Set(plainMessage.actions)); + const source = plainMessage.source; + const expandable = this.isExpandable(message, source, actions); + const collapsed = expandable; + const notification = { messageId, message, type, actions, expandable, collapsed }; + this.notifications.set(messageId, notification); + const result = new Deferred(); this.deferredResults.set(messageId, result); if (!this.centerVisible) { diff --git a/packages/metrics/package.json b/packages/metrics/package.json index f50c84618c93b..ffa673db971e4 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -1,10 +1,11 @@ { "name": "@theia/metrics", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Metrics Extension", "dependencies": { - "@theia/core": "1.44.0", - "prom-client": "^10.2.0" + "@theia/core": "1.54.0", + "prom-client": "^10.2.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -13,6 +14,9 @@ { "frontend": "lib/browser/metrics-frontend-module", "backend": "lib/node/metrics-backend-module" + }, + { + "backendElectron": "lib/electron-node/electron-metrics-backend-module" } ], "keywords": [ @@ -40,7 +44,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/metrics/src/electron-node/electron-metrics-backend-module.ts b/packages/metrics/src/electron-node/electron-metrics-backend-module.ts new file mode 100644 index 0000000000000..9daffa3ee5990 --- /dev/null +++ b/packages/metrics/src/electron-node/electron-metrics-backend-module.ts @@ -0,0 +1,24 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { MetricsElectronTokenValidator } from './electron-token-validator'; +import { ElectronTokenValidator } from '@theia/core/lib/electron-node/token/electron-token-validator'; + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(MetricsElectronTokenValidator).toSelf().inSingletonScope(); + rebind(ElectronTokenValidator).to(MetricsElectronTokenValidator); +}); diff --git a/packages/metrics/src/electron-node/electron-token-validator.ts b/packages/metrics/src/electron-node/electron-token-validator.ts new file mode 100644 index 0000000000000..202e9987dda2e --- /dev/null +++ b/packages/metrics/src/electron-node/electron-token-validator.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { injectable, postConstruct } from '@theia/core/shared/inversify'; +import { ElectronTokenValidator } from '@theia/core/lib/electron-node/token/electron-token-validator'; +import { IncomingMessage } from 'http'; +import { MetricsBackendApplicationContribution } from '../node/metrics-backend-application-contribution'; +import { MaybePromise } from '@theia/core'; + +@injectable() +export class MetricsElectronTokenValidator extends ElectronTokenValidator { + @postConstruct() + protected override init(): void { + super.init(); + } + + override allowWsUpgrade(request: IncomingMessage): MaybePromise { + return this.allowRequest(request); + } + + override allowRequest(request: IncomingMessage): boolean { + return request.url === MetricsBackendApplicationContribution.ENDPOINT || super.allowRequest(request); + } +} diff --git a/packages/metrics/src/node/metrics-backend-application-contribution.ts b/packages/metrics/src/node/metrics-backend-application-contribution.ts index b672e2e982bde..cba07eaf38b66 100644 --- a/packages/metrics/src/node/metrics-backend-application-contribution.ts +++ b/packages/metrics/src/node/metrics-backend-application-contribution.ts @@ -24,6 +24,7 @@ import { MetricsContribution } from './metrics-contribution'; @injectable() export class MetricsBackendApplicationContribution implements BackendApplicationContribution { + static ENDPOINT = '/metrics'; constructor( @inject(ContributionProvider) @named(MetricsContribution) protected readonly metricsProviders: ContributionProvider @@ -31,7 +32,7 @@ export class MetricsBackendApplicationContribution implements BackendApplication } configure(app: express.Application): void { - app.get('/metrics', (req, res) => { + app.get(MetricsBackendApplicationContribution.ENDPOINT, (req, res) => { const lastMetrics = this.fetchMetricsFromProviders(); res.send(lastMetrics); }); diff --git a/packages/mini-browser/package.json b/packages/mini-browser/package.json index 2e1a9cd5a5da9..2dbc4baf27487 100644 --- a/packages/mini-browser/package.json +++ b/packages/mini-browser/package.json @@ -1,14 +1,14 @@ { "name": "@theia/mini-browser", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Mini-Browser Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/filesystem": "1.44.0", + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", "@types/mime-types": "^2.1.0", "mime-types": "^2.1.18", "pdfobject": "^2.0.201604172", - "uuid": "^8.0.0", + "tslib": "^2.6.2", "vhost": "^3.0.2" }, "publishConfig": { @@ -49,7 +49,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/mini-browser/src/browser/environment/mini-browser-environment.ts b/packages/mini-browser/src/browser/environment/mini-browser-environment.ts index f5253f43708d6..b13b910d5638d 100644 --- a/packages/mini-browser/src/browser/environment/mini-browser-environment.ts +++ b/packages/mini-browser/src/browser/environment/mini-browser-environment.ts @@ -18,7 +18,7 @@ import { Endpoint, FrontendApplicationContribution } from '@theia/core/lib/brows import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { environment } from '@theia/core/shared/@theia/application-package/lib/environment'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import { MiniBrowserEndpoint } from '../../common/mini-browser-endpoint'; /** @@ -71,7 +71,7 @@ export class MiniBrowserEnvironment implements FrontendApplicationContribution { * Throws if `hostPatternPromise` is not yet resolved. */ getRandomEndpoint(): Endpoint { - return this.getEndpoint(v4()); + return this.getEndpoint(generateUuid()); } protected async getHostPattern(): Promise { diff --git a/packages/mini-browser/src/node/mini-browser-endpoint.ts b/packages/mini-browser/src/node/mini-browser-endpoint.ts index f8400b862bfff..1c3420bf5eba7 100644 --- a/packages/mini-browser/src/node/mini-browser-endpoint.ts +++ b/packages/mini-browser/src/node/mini-browser-endpoint.ts @@ -20,7 +20,7 @@ import * as fs from '@theia/core/shared/fs-extra'; import { lookup } from 'mime-types'; import { injectable, inject, named } from '@theia/core/shared/inversify'; import { Application, Request, Response } from '@theia/core/shared/express'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { ILogger } from '@theia/core/lib/common/logger'; import { MaybePromise } from '@theia/core/lib/common/types'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; diff --git a/packages/monaco/package.json b/packages/monaco/package.json index 6c4d20c85cff5..7e65c3cd66811 100644 --- a/packages/monaco/package.json +++ b/packages/monaco/package.json @@ -1,18 +1,19 @@ { "name": "@theia/monaco", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Monaco Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/markers": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/outline-view": "1.44.0", - "@theia/workspace": "1.44.0", + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/markers": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", + "@theia/outline-view": "1.54.0", + "@theia/workspace": "1.54.0", "fast-plist": "^0.1.2", "idb": "^4.0.5", "jsonc-parser": "^2.2.0", + "tslib": "^2.6.2", "vscode-oniguruma": "1.6.1", "vscode-textmate": "^9.0.0" }, @@ -21,7 +22,8 @@ }, "theiaExtensions": [ { - "frontend": "lib/browser/monaco-frontend-module" + "frontend": "lib/browser/monaco-frontend-module", + "secondaryWindow": "lib/browser/monaco-frontend-module" } ], "keywords": [ @@ -50,7 +52,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/monaco/src/browser/markdown-renderer/monaco-markdown-renderer.ts b/packages/monaco/src/browser/markdown-renderer/monaco-markdown-renderer.ts index 5f2852d94cba6..dd46b027c06d8 100644 --- a/packages/monaco/src/browser/markdown-renderer/monaco-markdown-renderer.ts +++ b/packages/monaco/src/browser/markdown-renderer/monaco-markdown-renderer.ts @@ -16,14 +16,11 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language'; -import { MarkdownRenderer as CodeMarkdownRenderer } from '@theia/monaco-editor-core/esm/vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; +import { MarkdownRenderer as CodeMarkdownRenderer, IMarkdownRendererOptions } from '@theia/monaco-editor-core/esm/vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; -import { MonacoCommandServiceFactory } from '../monaco-command-service'; -import { MonacoEditorService } from '../monaco-editor-service'; import * as monaco from '@theia/monaco-editor-core'; -import { OpenerService as MonacoOpenerService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/openerService'; import { OpenerService, PreferenceService, WidgetOpenerOptions, open } from '@theia/core/lib/browser'; -import { OpenExternalOptions, OpenInternalOptions } from '@theia/monaco-editor-core/esm/vs/platform/opener/common/opener'; +import { IOpenerService, OpenExternalOptions, OpenInternalOptions } from '@theia/monaco-editor-core/esm/vs/platform/opener/common/opener'; import { HttpOpenHandlerOptions } from '@theia/core/lib/browser/http-open-handler'; import { URI } from '@theia/core/lib/common/uri'; import { MarkdownRenderer, MarkdownRenderOptions, MarkdownRenderResult } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; @@ -34,8 +31,6 @@ import { DisposableCollection, DisposableGroup } from '@theia/core'; @injectable() export class MonacoMarkdownRenderer implements MarkdownRenderer { - @inject(MonacoEditorService) protected readonly codeEditorService: MonacoEditorService; - @inject(MonacoCommandServiceFactory) protected readonly commandServiceFactory: MonacoCommandServiceFactory; @inject(OpenerService) protected readonly openerService: OpenerService; @inject(PreferenceService) protected readonly preferences: PreferenceService; @@ -72,21 +67,18 @@ export class MonacoMarkdownRenderer implements MarkdownRenderer { @postConstruct() protected init(): void { const languages = StandaloneServices.get(ILanguageService); - const openerService = new MonacoOpenerService(this.codeEditorService, this.commandServiceFactory()); + const openerService = StandaloneServices.get(IOpenerService); openerService.registerOpener({ open: (u, options) => this.interceptOpen(u, options) }); - const getPreference = () => this.preferences.get('editor.fontFamily'); - const rendererOptions = new Proxy(Object.create(null), { // eslint-disable-line no-null/no-null - get(_, field): string | undefined { - if (field === 'codeBlockFontFamily') { - return getPreference(); - } else { - return undefined; - } + const that = this; + const prefs = new class implements IMarkdownRendererOptions { + get codeBlockFontFamily(): string | undefined { + return that.preferences.get('editor.fontFamily'); } - }); - this.delegate = new CodeMarkdownRenderer(rendererOptions, languages, openerService); + }; + + this.delegate = new CodeMarkdownRenderer(prefs, languages, openerService); } protected async interceptOpen(monacoUri: monaco.Uri | string, monacoOptions?: OpenInternalOptions | OpenExternalOptions): Promise { diff --git a/packages/monaco/src/browser/monaco-bulk-edit-service.ts b/packages/monaco/src/browser/monaco-bulk-edit-service.ts index f1d614c63bccf..a5f7accc1b2d3 100644 --- a/packages/monaco/src/browser/monaco-bulk-edit-service.ts +++ b/packages/monaco/src/browser/monaco-bulk-edit-service.ts @@ -31,12 +31,12 @@ export class MonacoBulkEditService implements IBulkEditService { private _previewHandler?: IBulkEditPreviewHandler; - async apply(editsIn: ResourceEdit[] | WorkspaceEdit, options?: IBulkEditOptions): Promise { + async apply(editsIn: ResourceEdit[] | WorkspaceEdit, options?: IBulkEditOptions): Promise { const edits = Array.isArray(editsIn) ? editsIn : ResourceEdit.convert(editsIn); if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { editsIn = await this._previewHandler(edits, options); - return { ariaSummary: '', success: true }; + return { ariaSummary: '', isApplied: true }; } else { return this.workspace.applyBulkEdit(edits, options); } diff --git a/packages/monaco/src/browser/monaco-command-registry.ts b/packages/monaco/src/browser/monaco-command-registry.ts index e9b76e454b373..540425850460f 100644 --- a/packages/monaco/src/browser/monaco-command-registry.ts +++ b/packages/monaco/src/browser/monaco-command-registry.ts @@ -36,7 +36,10 @@ export class MonacoCommandRegistry { @inject(SelectionService) protected readonly selectionService: SelectionService; - validate(command: string): string | undefined { + validate(command: string | undefined): string | undefined { + if (!command) { + return undefined; + } return this.commands.commandIds.indexOf(command) !== -1 ? command : undefined; } diff --git a/packages/monaco/src/browser/monaco-command-service.ts b/packages/monaco/src/browser/monaco-command-service.ts index 7334eff649435..b6da221068cf6 100644 --- a/packages/monaco/src/browser/monaco-command-service.ts +++ b/packages/monaco/src/browser/monaco-command-service.ts @@ -14,18 +14,14 @@ // 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { CommandRegistry } from '@theia/core/lib/common/command'; import { Emitter } from '@theia/core/lib/common/event'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { ICommandEvent, ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands'; -import { StandaloneCommandService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { StandaloneCommandService, StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import * as monaco from '@theia/monaco-editor-core'; - -export const MonacoCommandServiceFactory = Symbol('MonacoCommandServiceFactory'); -export interface MonacoCommandServiceFactory { - (): MonacoCommandService; -} +import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation'; @injectable() export class MonacoCommandService implements ICommandService, Disposable { @@ -38,7 +34,6 @@ export class MonacoCommandService implements ICommandService, Disposable { ); protected delegate: StandaloneCommandService | undefined; - protected readonly delegateListeners = new DisposableCollection(); constructor( @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry @@ -47,6 +42,19 @@ export class MonacoCommandService implements ICommandService, Disposable { this.toDispose.push(this.commandRegistry.onDidExecuteCommand(e => this.onDidExecuteCommandEmitter.fire(e))); } + @postConstruct() + init(): void { + this.delegate = new StandaloneCommandService(StandaloneServices.get(IInstantiationService)); + if (this.delegate) { + this.toDispose.push(this.delegate.onWillExecuteCommand(event => + this.onWillExecuteCommandEmitter.fire(event) + )); + this.toDispose.push(this.delegate.onDidExecuteCommand(event => + this.onDidExecuteCommandEmitter.fire(event) + )); + } + } + dispose(): void { this.toDispose.dispose(); } @@ -59,23 +67,6 @@ export class MonacoCommandService implements ICommandService, Disposable { return this.onDidExecuteCommandEmitter.event; } - setDelegate(delegate: StandaloneCommandService | undefined): void { - if (this.toDispose.disposed) { - return; - } - this.delegateListeners.dispose(); - this.toDispose.push(this.delegateListeners); - this.delegate = delegate; - if (this.delegate) { - this.delegateListeners.push(this.delegate.onWillExecuteCommand(event => - this.onWillExecuteCommandEmitter.fire(event) - )); - this.delegateListeners.push(this.delegate.onDidExecuteCommand(event => - this.onDidExecuteCommandEmitter.fire(event) - )); - } - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any async executeCommand(commandId: any, ...args: any[]): Promise { try { diff --git a/packages/monaco/src/browser/monaco-command.ts b/packages/monaco/src/browser/monaco-command.ts index 323f4408a655f..0b194b4397a6f 100644 --- a/packages/monaco/src/browser/monaco-command.ts +++ b/packages/monaco/src/browser/monaco-command.ts @@ -21,34 +21,33 @@ import { CommonCommands, QuickInputService, ApplicationShell } from '@theia/core import { EditorCommands, EditorManager, EditorWidget } from '@theia/editor/lib/browser'; import { MonacoEditor } from './monaco-editor'; import { MonacoCommandRegistry, MonacoEditorCommandHandler } from './monaco-command-registry'; -import { MonacoEditorService } from './monaco-editor-service'; -import { MonacoTextModelService } from './monaco-text-model-service'; import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter'; import { nls } from '@theia/core/lib/common/nls'; -import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService'; import { EditorExtensionsRegistry } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorExtensions'; import { CommandsRegistry, ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands'; import * as monaco from '@theia/monaco-editor-core'; import { EndOfLineSequence } from '@theia/monaco-editor-core/esm/vs/editor/common/model'; import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation'; +import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; export namespace MonacoCommands { export const COMMON_ACTIONS = new Map([ - ['undo', CommonCommands.UNDO.id], - ['redo', CommonCommands.REDO.id], ['editor.action.selectAll', CommonCommands.SELECT_ALL.id], ['actions.find', CommonCommands.FIND.id], - ['editor.action.startFindReplaceAction', CommonCommands.REPLACE.id] + ['editor.action.startFindReplaceAction', CommonCommands.REPLACE.id], + ['editor.action.clipboardCutAction', CommonCommands.CUT.id], + ['editor.action.clipboardCopyAction', CommonCommands.COPY.id], + ['editor.action.clipboardPasteAction', CommonCommands.PASTE.id] ]); export const GO_TO_DEFINITION = 'editor.action.revealDefinition'; export const EXCLUDE_ACTIONS = new Set([ 'editor.action.quickCommand', - 'editor.action.clipboardCutAction', - 'editor.action.clipboardCopyAction', - 'editor.action.clipboardPasteAction' + 'undo', + 'redo' ]); } @@ -67,15 +66,6 @@ export class MonacoEditorCommandHandlers implements CommandContribution { @inject(QuickInputService) @optional() protected readonly quickInputService: QuickInputService; - @inject(MonacoEditorService) - protected readonly codeEditorService: MonacoEditorService; - - @inject(MonacoTextModelService) - protected readonly textModelService: MonacoTextModelService; - - @inject(VSCodeContextKeyService) - protected readonly contextKeyService: VSCodeContextKeyService; - @inject(ApplicationShell) protected readonly shell: ApplicationShell; @@ -135,10 +125,10 @@ export class MonacoEditorCommandHandlers implements CommandContribution { * and execute them using the instantiation service of the current editor. */ protected registerMonacoCommands(): void { - const editorActions = new Map(EditorExtensionsRegistry.getEditorActions().map(({ id, label, alias }) => [id, { label, alias }])); + const editorActions = new Map([...EditorExtensionsRegistry.getEditorActions()].map(({ id, label, alias }) => [id, { label, alias }])); - const { codeEditorService } = this; - const globalInstantiationService = StandaloneServices.initialize({}); + const codeEditorService = StandaloneServices.get(ICodeEditorService); + const globalInstantiationService = StandaloneServices.get(IInstantiationService); const monacoCommands = CommandsRegistry.getCommands(); for (const id of monacoCommands.keys()) { if (MonacoCommands.EXCLUDE_ACTIONS.has(id)) { @@ -173,7 +163,7 @@ export class MonacoEditorCommandHandlers implements CommandContribution { const action = editor && editor.getAction(id); return !!action && action.isSupported(); } - if (!!EditorExtensionsRegistry.getEditorCommand(id)) { + if (!!EditorExtensionsRegistry.getEditorCommand(id) || MonacoCommands.COMMON_ACTIONS.has(id)) { return !!editor; } return true; diff --git a/packages/monaco/src/browser/monaco-context-key-service.ts b/packages/monaco/src/browser/monaco-context-key-service.ts index 6450f4240d2d1..38640ba236c00 100644 --- a/packages/monaco/src/browser/monaco-context-key-service.ts +++ b/packages/monaco/src/browser/monaco-context-key-service.ts @@ -14,22 +14,24 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; +import { injectable, postConstruct } from '@theia/core/shared/inversify'; import { ContextKeyService as TheiaContextKeyService, ContextKey, ContextKeyChangeEvent, ScopedValueStore, ContextMatcher, ContextKeyValue } from '@theia/core/lib/browser/context-key-service'; import { Emitter } from '@theia/core'; -import { AbstractContextKeyService, ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService'; +import { AbstractContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService'; import { ContextKeyExpr, ContextKeyExpression, IContext, IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; @injectable() export class MonacoContextKeyService implements TheiaContextKeyService { protected readonly onDidChangeEmitter = new Emitter(); readonly onDidChange = this.onDidChangeEmitter.event; - @inject(VSCodeContextKeyService) - protected readonly contextKeyService: VSCodeContextKeyService; + get contextKeyService(): AbstractContextKeyService { + return StandaloneServices.get(IContextKeyService) as AbstractContextKeyService; + } @postConstruct() protected init(): void { @@ -107,9 +109,9 @@ export class MonacoContextKeyService implements TheiaContextKeyService { createScoped(target: HTMLElement): ScopedValueStore { const scoped = this.contextKeyService.createScoped(target); if (scoped instanceof AbstractContextKeyService) { - return scoped as AbstractContextKeyService & { createScoped(): ScopedValueStore }; + return scoped as unknown as ScopedValueStore; } - return this; + throw new Error('Could not created scoped value store'); } createOverlay(overlay: Iterable<[string, unknown]>): ContextMatcher { @@ -125,8 +127,7 @@ export class MonacoContextKeyService implements TheiaContextKeyService { return parsed.evaluate(ctx); } return true; - }, - dispose: () => delegate.dispose(), + } }; } diff --git a/packages/monaco/src/browser/monaco-context-menu.ts b/packages/monaco/src/browser/monaco-context-menu.ts index 5049414bf83b9..ad4f7239de0d2 100644 --- a/packages/monaco/src/browser/monaco-context-menu.ts +++ b/packages/monaco/src/browser/monaco-context-menu.ts @@ -17,13 +17,14 @@ import { injectable, inject } from '@theia/core/shared/inversify'; import { MenuPath } from '@theia/core/lib/common/menu'; import { EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser'; -import { ContextMenuRenderer, toAnchor } from '@theia/core/lib/browser'; +import { Anchor, ContextMenuRenderer, Coordinate } from '@theia/core/lib/browser'; import { Menu } from '@theia/core/shared/@phosphor/widgets'; import { CommandRegistry } from '@theia/core/shared/@phosphor/commands'; import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView'; import { IContextMenuDelegate } from '@theia/monaco-editor-core/esm/vs/base/browser/contextmenu'; import { MenuItemAction } from '@theia/monaco-editor-core/esm/vs/platform/actions/common/actions'; import { Event, Emitter } from '@theia/monaco-editor-core/esm/vs/base/common/event'; +import { StandardMouseEvent } from '@theia/monaco-editor-core/esm/vs/base/browser/mouseEvent'; @injectable() export class MonacoContextMenuService implements IContextMenuService { @@ -38,12 +39,32 @@ export class MonacoContextMenuService implements IContextMenuService { return this.onDidShowContextMenuEmitter.event; }; - constructor(@inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer) { + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; + + toAnchor(anchor: HTMLElement | Coordinate | StandardMouseEvent): Anchor { + if (anchor instanceof HTMLElement) { + return { x: anchor.offsetLeft, y: anchor.offsetTop }; + } else if (anchor instanceof StandardMouseEvent) { + return { x: anchor.posx, y: anchor.posy }; + } else { + return anchor; + } } + private getContext(delegate: IContextMenuDelegate): HTMLElement | undefined { + const anchor = delegate.getAnchor(); + if (anchor instanceof HTMLElement) { + return anchor; + } else if (anchor instanceof StandardMouseEvent) { + return anchor.target; + } else { + return undefined; + } + } showContextMenu(delegate: IContextMenuDelegate): void { - const anchor = toAnchor(delegate.getAnchor()); + const anchor = this.toAnchor(delegate.getAnchor()); const actions = delegate.getActions(); + const context = this.getContext(delegate); const onHide = () => { delegate.onHide?.(false); this.onDidHideContextMenuEmitter.fire(); @@ -53,6 +74,7 @@ export class MonacoContextMenuService implements IContextMenuService { // In case of 'Quick Fix' actions come as 'CodeActionAction' items if (actions.length > 0 && actions[0] instanceof MenuItemAction) { this.contextMenuRenderer.render({ + context: context, menuPath: this.menuPath(), anchor, onHide diff --git a/packages/monaco/src/browser/monaco-diff-editor.ts b/packages/monaco/src/browser/monaco-diff-editor.ts index fa001542240d4..22f6722e36230 100644 --- a/packages/monaco/src/browser/monaco-diff-editor.ts +++ b/packages/monaco/src/browser/monaco-diff-editor.ts @@ -22,17 +22,23 @@ import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './mo import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; import * as monaco from '@theia/monaco-editor-core'; -import { IDiffEditorConstructionOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; -import { IDiffNavigatorOptions } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneEditor'; -import { StandaloneDiffEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor'; +import { ICodeEditor, IDiffEditorConstructionOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; +import { IActionDescriptor, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneCodeEditor, StandaloneDiffEditor2 } + from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor'; +import { IEditorConstructionOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/config/editorConfiguration'; +import { EmbeddedDiffEditorWidget } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation'; +import { ContextKeyValue, IContextKey } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; +import { IDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle'; +import { ICommandHandler } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands'; export namespace MonacoDiffEditor { - export interface IOptions extends MonacoEditor.ICommonOptions, IDiffEditorConstructionOptions, IDiffNavigatorOptions { + export interface IOptions extends MonacoEditor.ICommonOptions, IDiffEditorConstructionOptions { } } export class MonacoDiffEditor extends MonacoEditor { - protected _diffEditor: monaco.editor.IStandaloneDiffEditor; + protected _diffEditor: IStandaloneDiffEditor; protected _diffNavigator: DiffNavigator; constructor( @@ -44,17 +50,18 @@ export class MonacoDiffEditor extends MonacoEditor { protected readonly diffNavigatorFactory: MonacoDiffNavigatorFactory, options?: MonacoDiffEditor.IOptions, override?: EditorServiceOverrides, + parentEditor?: MonacoEditor ) { - super(uri, modifiedModel, node, services, options, override); + super(uri, modifiedModel, node, services, options, override, parentEditor); this.documents.add(originalModel); const original = originalModel.textEditorModel; const modified = modifiedModel.textEditorModel; - this._diffNavigator = diffNavigatorFactory.createdDiffNavigator(this._diffEditor, options); + this._diffNavigator = diffNavigatorFactory.createdDiffNavigator(this._diffEditor); this._diffEditor.setModel({ original, modified }); } get diffEditor(): monaco.editor.IStandaloneDiffEditor { - return this._diffEditor; + return this._diffEditor as unknown as monaco.editor.IStandaloneDiffEditor; } get diffNavigator(): DiffNavigator { @@ -62,14 +69,16 @@ export class MonacoDiffEditor extends MonacoEditor { } protected override create(options?: IDiffEditorConstructionOptions, override?: EditorServiceOverrides): Disposable { + options = { ...options, fixedOverflowWidgets: true }; const instantiator = this.getInstantiatorWithOverrides(override); /** * @monaco-uplift. Should be guaranteed to work. * Incomparable enums prevent TypeScript from believing that public IStandaloneDiffEditor is satisfied by private StandaloneDiffEditor */ - this._diffEditor = instantiator - .createInstance(StandaloneDiffEditor, this.node, { ...options, fixedOverflowWidgets: true }) as unknown as monaco.editor.IStandaloneDiffEditor; - this.editor = this._diffEditor.getModifiedEditor(); + this._diffEditor = this.parentEditor ? + instantiator.createInstance(EmbeddedDiffEditor, this.node, options, {}, this.parentEditor.getControl() as unknown as ICodeEditor) : + instantiator.createInstance(StandaloneDiffEditor2, this.node, options); + this.editor = this._diffEditor.getModifiedEditor() as unknown as monaco.editor.IStandaloneCodeEditor; return this._diffEditor; } @@ -98,4 +107,35 @@ export class MonacoDiffEditor extends MonacoEditor { return DiffUris.encode(left.withPath(resourceUri.path), right.withPath(resourceUri.path)); } + override shouldDisplayDirtyDiff(): boolean { + return false; + } +} + +class EmbeddedDiffEditor extends EmbeddedDiffEditorWidget implements IStandaloneDiffEditor { + + protected override _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, + options: Readonly): StandaloneCodeEditor { + return instantiationService.createInstance(StandaloneCodeEditor, container, options); + } + + override getOriginalEditor(): IStandaloneCodeEditor { + return super.getOriginalEditor() as IStandaloneCodeEditor; + } + + override getModifiedEditor(): IStandaloneCodeEditor { + return super.getModifiedEditor() as IStandaloneCodeEditor; + } + + addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null { + return this.getModifiedEditor().addCommand(keybinding, handler, context); + } + + createContextKey(key: string, defaultValue: T): IContextKey { + return this.getModifiedEditor().createContextKey(key, defaultValue); + } + + addAction(descriptor: IActionDescriptor): IDisposable { + return this.getModifiedEditor().addAction(descriptor); + } } diff --git a/packages/monaco/src/browser/monaco-diff-navigator-factory.ts b/packages/monaco/src/browser/monaco-diff-navigator-factory.ts index 906057465ed0e..897aaa611359d 100644 --- a/packages/monaco/src/browser/monaco-diff-navigator-factory.ts +++ b/packages/monaco/src/browser/monaco-diff-navigator-factory.ts @@ -16,46 +16,24 @@ import { injectable } from '@theia/core/shared/inversify'; import { DiffNavigator } from '@theia/editor/lib/browser'; -import * as monaco from '@theia/monaco-editor-core'; -import { DiffNavigator as MonacoDiffNavigator } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/diffNavigator'; -import { IStandaloneDiffEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor'; +import { IDiffEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; @injectable() export class MonacoDiffNavigatorFactory { static nullNavigator = { - canNavigate: () => false, hasNext: () => false, hasPrevious: () => false, next: () => { }, previous: () => { }, }; - createdDiffNavigator(editor: IStandaloneDiffEditor | monaco.editor.IStandaloneDiffEditor, options?: monaco.editor.IDiffNavigatorOptions): DiffNavigator { - const navigator = new MonacoDiffNavigator(editor as IStandaloneDiffEditor, options); - const ensureInitialized = (fwd: boolean) => { - if (navigator['nextIdx'] < 0) { - navigator['_initIdx'](fwd); - } - }; + createdDiffNavigator(editor: IDiffEditor): DiffNavigator { return { - canNavigate: () => navigator.canNavigate(), - hasNext: () => { - if (navigator.canNavigate()) { - ensureInitialized(true); - return navigator['nextIdx'] + 1 < navigator['ranges'].length; - } - return false; - }, - hasPrevious: () => { - if (navigator.canNavigate()) { - ensureInitialized(false); - return navigator['nextIdx'] > 0; - } - return false; - }, - next: () => navigator.next(), - previous: () => navigator.previous(), + hasNext: () => true, + hasPrevious: () => true, + next: () => editor.goToDiff('next'), + previous: () => editor.goToDiff('previous') }; } } diff --git a/packages/monaco/src/browser/monaco-editor-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts index f2f44cd6776b8..9e0651118a8ba 100644 --- a/packages/monaco/src/browser/monaco-editor-model.ts +++ b/packages/monaco/src/browser/monaco-editor-model.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Position, Range, TextDocumentSaveReason, TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-languageserver-protocol'; +import { Position, Range, TextDocumentSaveReason } from '@theia/core/shared/vscode-languageserver-protocol'; import { TextEditorDocument, EncodingMode, FindMatchesOptions, FindMatch, EditorPreferences } from '@theia/editor/lib/browser'; import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; import { Emitter, Event } from '@theia/core/lib/common/event'; @@ -32,6 +32,8 @@ import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common import { IModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/model'; import { createTextBufferFactoryFromStream } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel'; import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/browser/editor-generated-preference-schema'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; export { TextDocumentSaveReason @@ -46,13 +48,18 @@ export interface WillSaveMonacoModelEvent { export interface MonacoModelContentChangedEvent { readonly model: MonacoEditorModel; - readonly contentChanges: TextDocumentContentChangeEvent[]; + readonly contentChanges: MonacoTextDocumentContentChange[]; +} + +export interface MonacoTextDocumentContentChange { + readonly range: Range; + readonly rangeOffset: number; + readonly rangeLength: number; + readonly text: string; } export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDocument { - autoSave: EditorPreferences['files.autoSave'] = 'afterDelay'; - autoSaveDelay = 500; suppressOpenEditorWhenDirty = false; lineNumbersMinChars = 3; @@ -69,6 +76,10 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo protected readonly onDidChangeContentEmitter = new Emitter(); readonly onDidChangeContent = this.onDidChangeContentEmitter.event; + get onContentChanged(): Event { + return (listener, thisArgs, disposables) => this.onDidChangeContent(() => listener(), thisArgs, disposables); + } + protected readonly onDidSaveModelEmitter = new Emitter(); readonly onDidSaveModel = this.onDidSaveModelEmitter.event; @@ -81,6 +92,8 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo protected readonly onDidChangeEncodingEmitter = new Emitter(); readonly onDidChangeEncoding = this.onDidChangeEncodingEmitter.event; + readonly onDidChangeReadOnly: Event = this.resource.onDidChangeReadOnly ?? Event.None; + private preferredEncoding: string | undefined; private contentEncoding: string | undefined; @@ -107,6 +120,14 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo ); } + undo(): void { + this.model.undo(); + } + + redo(): void { + this.model.redo(); + } + dispose(): void { this.toDispose.dispose(); } @@ -302,11 +323,11 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo return this.m2p.asRange(this.model.validateRange(this.p2m.asRange(range))); } - get readOnly(): boolean { - return this.resource.saveContents === undefined; + get readOnly(): boolean | MarkdownString { + return this.resource.readOnly ?? false; } - isReadonly(): boolean { + isReadonly(): boolean | MarkdownString { return this.readOnly; } @@ -361,7 +382,7 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo } save(options?: SaveOptions): Promise { - return this.scheduleSave(TextDocumentSaveReason.Manual, undefined, undefined, options); + return this.scheduleSave(options?.saveReason ?? TextDocumentSaveReason.Manual, undefined, undefined, options); } protected pendingOperation = Promise.resolve(); @@ -449,23 +470,9 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo } this.cancelSync(); this.setDirty(true); - this.doAutoSave(); this.trace(log => log('MonacoEditorModel.markAsDirty - exit')); } - protected doAutoSave(): void { - if (this.autoSave !== 'off' && this.resource.uri.scheme !== UNTITLED_SCHEME) { - const token = this.cancelSave(); - this.toDisposeOnAutoSave.dispose(); - const handle = window.setTimeout(() => { - this.scheduleSave(TextDocumentSaveReason.AfterDelay, token); - }, this.autoSaveDelay); - this.toDisposeOnAutoSave.push(Disposable.create(() => - window.clearTimeout(handle)) - ); - } - } - protected saveCancellationTokenSource = new CancellationTokenSource(); protected cancelSave(): CancellationToken { this.trace(log => log('MonacoEditorModel.cancelSave')); @@ -479,8 +486,8 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo } protected ignoreContentChanges = false; - protected readonly contentChanges: TextDocumentContentChangeEvent[] = []; - protected pushContentChanges(contentChanges: TextDocumentContentChangeEvent[]): void { + protected readonly contentChanges: MonacoTextDocumentContentChange[] = []; + protected pushContentChanges(contentChanges: MonacoTextDocumentContentChange[]): void { if (!this.ignoreContentChanges) { this.contentChanges.push(...contentChanges); } @@ -503,11 +510,12 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo const contentChanges = event.changes.map(change => this.asTextDocumentContentChangeEvent(change)); return { model: this, contentChanges }; } - protected asTextDocumentContentChangeEvent(change: monaco.editor.IModelContentChange): TextDocumentContentChangeEvent { + protected asTextDocumentContentChangeEvent(change: monaco.editor.IModelContentChange): MonacoTextDocumentContentChange { const range = this.m2p.asRange(change.range); + const rangeOffset = change.rangeOffset; const rangeLength = change.rangeLength; const text = change.text; - return { range, rangeLength, text }; + return { range, rangeOffset, rangeLength, text }; } protected applyEdits( @@ -660,10 +668,14 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo } applySnapshot(snapshot: Saveable.Snapshot): void { - const value = 'value' in snapshot ? snapshot.value : snapshot.read() ?? ''; + const value = Saveable.Snapshot.read(snapshot) ?? ''; this.model.setValue(value); } + async serialize(): Promise { + return BinaryBuffer.fromString(this.model.getValue()); + } + protected trace(loggable: Loggable): void { if (this.logger) { this.logger.debug((log: Log) => diff --git a/packages/monaco/src/browser/monaco-editor-peek-view-widget.ts b/packages/monaco/src/browser/monaco-editor-peek-view-widget.ts new file mode 100644 index 0000000000000..4c5d90b455c34 --- /dev/null +++ b/packages/monaco/src/browser/monaco-editor-peek-view-widget.ts @@ -0,0 +1,233 @@ +// ***************************************************************************** +// Copyright (C) 2024 1C-Soft LLC 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 { Position, Range } from '@theia/core/shared/vscode-languageserver-protocol'; +import { DisposableCollection } from '@theia/core'; +import { MonacoEditor } from './monaco-editor'; +import * as monaco from '@theia/monaco-editor-core'; +import { PeekViewWidget, IPeekViewOptions, IPeekViewStyles } from '@theia/monaco-editor-core/esm/vs/editor/contrib/peekView/browser/peekView'; +import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; +import { ActionBar } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/actionbar/actionbar'; +import { Action } from '@theia/monaco-editor-core/esm/vs/base/common/actions'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation'; +import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { Color } from '@theia/monaco-editor-core/esm/vs/base/common/color'; + +export { peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground } + from '@theia/monaco-editor-core/esm/vs/editor/contrib/peekView/browser/peekView'; + +export namespace MonacoEditorPeekViewWidget { + export interface Styles { + frameColor?: string; + arrowColor?: string; + headerBackgroundColor?: string; + primaryHeadingColor?: string; + secondaryHeadingColor?: string; + } + export interface Options { + showFrame?: boolean; + showArrow?: boolean; + frameWidth?: number; + className?: string; + isAccessible?: boolean; + isResizeable?: boolean; + keepEditorSelection?: boolean; + allowUnlimitedHeight?: boolean; + ordinal?: number; + showInHiddenAreas?: boolean; + supportOnTitleClick?: boolean; + } + export interface Action { + readonly id: string; + label: string; + tooltip: string; + class: string | undefined; + enabled: boolean; + checked?: boolean; + run(...args: unknown[]): unknown; + } + export interface ActionOptions { + icon?: boolean; + label?: boolean; + keybinding?: string; + index?: number; + } +} + +export class MonacoEditorPeekViewWidget { + + protected readonly toDispose = new DisposableCollection(); + + readonly onDidClose = this.toDispose.onDispose; + + private readonly themeService = StandaloneServices.get(IThemeService); + + private readonly delegate; + + constructor( + readonly editor: MonacoEditor, + options: MonacoEditorPeekViewWidget.Options = {}, + protected styles: MonacoEditorPeekViewWidget.Styles = {} + ) { + const that = this; + this.toDispose.push(this.delegate = new class extends PeekViewWidget { + + get actionBar(): ActionBar | undefined { + return this._actionbarWidget; + } + + fillHead(container: HTMLElement, noCloseAction?: boolean): void { + super._fillHead(container, noCloseAction); + } + + protected override _fillHead(container: HTMLElement, noCloseAction?: boolean): void { + that.fillHead(container, noCloseAction); + } + + fillBody(container: HTMLElement): void { + // super._fillBody is an abstract method + } + + protected override _fillBody(container: HTMLElement): void { + that.fillBody(container); + }; + + doLayoutHead(heightInPixel: number, widthInPixel: number): void { + super._doLayoutHead(heightInPixel, widthInPixel); + } + + protected override _doLayoutHead(heightInPixel: number, widthInPixel: number): void { + that.doLayoutHead(heightInPixel, widthInPixel); + } + + doLayoutBody(heightInPixel: number, widthInPixel: number): void { + super._doLayoutBody(heightInPixel, widthInPixel); + } + + protected override _doLayoutBody(heightInPixel: number, widthInPixel: number): void { + that.doLayoutBody(heightInPixel, widthInPixel); + } + + onWidth(widthInPixel: number): void { + super._onWidth(widthInPixel); + } + + protected override _onWidth(widthInPixel: number): void { + that.onWidth(widthInPixel); + } + + doRevealRange(range: monaco.Range, isLastLine: boolean): void { + super.revealRange(range, isLastLine); + } + + protected override revealRange(range: monaco.Range, isLastLine: boolean): void { + that.doRevealRange(that.editor['m2p'].asRange(range), isLastLine); + } + }( + editor.getControl() as unknown as ICodeEditor, + Object.assign({}, options, this.convertStyles(styles)), + StandaloneServices.get(IInstantiationService) + )); + this.toDispose.push(this.themeService.onDidColorThemeChange(() => this.style(this.styles))); + } + + dispose(): void { + this.toDispose.dispose(); + } + + create(): void { + this.delegate.create(); + } + + setTitle(primaryHeading: string, secondaryHeading?: string): void { + this.delegate.setTitle(primaryHeading, secondaryHeading); + } + + style(styles: MonacoEditorPeekViewWidget.Styles): void { + this.delegate.style(this.convertStyles(this.styles = styles)); + } + + show(rangeOrPos: Range | Position, heightInLines: number): void { + this.delegate.show(this.convertRangeOrPosition(rangeOrPos), heightInLines); + } + + hide(): void { + this.delegate.hide(); + } + + clearActions(): void { + this.delegate.actionBar?.clear(); + } + + addAction(id: string, label: string, cssClass: string | undefined, enabled: boolean, actionCallback: (arg: unknown) => unknown, + options?: MonacoEditorPeekViewWidget.ActionOptions): MonacoEditorPeekViewWidget.Action { + options = cssClass ? { icon: true, label: false, ...options } : { icon: false, label: true, ...options }; + const { actionBar } = this.delegate; + if (!actionBar) { + throw new Error('Action bar has not been created.'); + } + const action = new Action(id, label, cssClass, enabled, actionCallback); + actionBar.push(action, options); + return action; + } + + protected fillHead(container: HTMLElement, noCloseAction?: boolean): void { + this.delegate.fillHead(container, noCloseAction); + } + + protected fillBody(container: HTMLElement): void { + this.delegate.fillBody(container); + } + + protected doLayoutHead(heightInPixel: number, widthInPixel: number): void { + this.delegate.doLayoutHead(heightInPixel, widthInPixel); + } + + protected doLayoutBody(heightInPixel: number, widthInPixel: number): void { + this.delegate.doLayoutBody(heightInPixel, widthInPixel); + } + + protected onWidth(widthInPixel: number): void { + this.delegate.onWidth(widthInPixel); + } + + protected doRevealRange(range: Range, isLastLine: boolean): void { + this.delegate.doRevealRange(this.editor['p2m'].asRange(range), isLastLine); + } + + private convertStyles(styles: MonacoEditorPeekViewWidget.Styles): IPeekViewStyles { + return { + frameColor: this.convertColor(styles.frameColor), + arrowColor: this.convertColor(styles.arrowColor), + headerBackgroundColor: this.convertColor(styles.headerBackgroundColor), + primaryHeadingColor: this.convertColor(styles.primaryHeadingColor), + secondaryHeadingColor: this.convertColor(styles.secondaryHeadingColor), + }; + } + + private convertColor(color?: string): Color | undefined { + if (color === undefined) { + return undefined; + } + return this.themeService.getColorTheme().getColor(color) || Color.fromHex(color); + } + + private convertRangeOrPosition(arg: Range | Position): monaco.Range | monaco.Position { + const p2m = this.editor['p2m']; + return Range.is(arg) ? p2m.asRange(arg) : p2m.asPosition(arg); + } +} diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index e939ee5f2cc6c..75fa4dbcb049a 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -21,17 +21,11 @@ import { DiffUris } from '@theia/core/lib/browser/diff-uris'; import { inject, injectable, named } from '@theia/core/shared/inversify'; import { DisposableCollection, deepClone, Disposable } from '@theia/core/lib/common'; import { TextDocumentSaveReason } from '@theia/core/shared/vscode-languageserver-protocol'; -import { MonacoCommandServiceFactory } from './monaco-command-service'; -import { MonacoContextMenuService } from './monaco-context-menu'; import { MonacoDiffEditor } from './monaco-diff-editor'; import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory'; import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor'; import { MonacoEditorModel, WillSaveMonacoModelEvent } from './monaco-editor-model'; -import { MonacoEditorService } from './monaco-editor-service'; -import { MonacoTextModelService } from './monaco-text-model-service'; import { MonacoWorkspace } from './monaco-workspace'; -import { MonacoBulkEditService } from './monaco-bulk-edit-service'; -import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; import { ContributionProvider } from '@theia/core'; import { KeybindingRegistry, OpenerService, open, WidgetOpenerOptions, FormatType } from '@theia/core/lib/browser'; import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding'; @@ -39,23 +33,17 @@ import { HttpOpenHandlerOptions } from '@theia/core/lib/browser/http-open-handle import { MonacoToProtocolConverter } from './monaco-to-protocol-converter'; import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter'; import { FileSystemPreferences } from '@theia/filesystem/lib/browser'; -import { MonacoQuickInputImplementation } from './monaco-quick-input-service'; -import { ContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService'; import * as monaco from '@theia/monaco-editor-core'; -import { OpenerService as MonacoOpenerService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/openerService'; -import { StandaloneCommandService, StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { IOpenerService, OpenExternalOptions, OpenInternalOptions } from '@theia/monaco-editor-core/esm/vs/platform/opener/common/opener'; -import { SimpleKeybinding } from '@theia/monaco-editor-core/esm/vs/base/common/keybindings'; -import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; -import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from '@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybinding'; import { timeoutReject } from '@theia/core/lib/common/promise-util'; -import { ITextModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService'; import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView'; -import { IBulkEditService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService'; +import { KeyCodeChord } from '@theia/monaco-editor-core/esm/vs/base/common/keybindings'; import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; -import { IQuickInputService } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickInput'; -import { ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands'; +import { ITextModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService'; +import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export const MonacoEditorFactory = Symbol('MonacoEditorFactory'); export interface MonacoEditorFactory { @@ -70,9 +58,6 @@ export class MonacoEditorProvider { @named(MonacoEditorFactory) protected readonly factories: ContributionProvider; - @inject(MonacoBulkEditService) - protected readonly bulkEditService: MonacoBulkEditService; - @inject(MonacoEditorServices) protected readonly services: MonacoEditorServices; @@ -85,9 +70,6 @@ export class MonacoEditorProvider { @inject(FileSystemPreferences) protected readonly filePreferences: FileSystemPreferences; - @inject(MonacoQuickInputImplementation) - protected readonly quickInputService: MonacoQuickInputImplementation; - protected _current: MonacoEditor | undefined; /** * Returns the last focused MonacoEditor. @@ -99,26 +81,16 @@ export class MonacoEditorProvider { } constructor( - @inject(MonacoEditorService) protected readonly codeEditorService: MonacoEditorService, - @inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService, - @inject(MonacoContextMenuService) protected readonly contextMenuService: MonacoContextMenuService, @inject(MonacoToProtocolConverter) protected readonly m2p: MonacoToProtocolConverter, @inject(ProtocolToMonacoConverter) protected readonly p2m: ProtocolToMonacoConverter, @inject(MonacoWorkspace) protected readonly workspace: MonacoWorkspace, - @inject(MonacoCommandServiceFactory) protected readonly commandServiceFactory: MonacoCommandServiceFactory, @inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences, @inject(MonacoDiffNavigatorFactory) protected readonly diffNavigatorFactory: MonacoDiffNavigatorFactory, - /** @deprecated since 1.6.0 */ - @inject(ApplicationServer) protected readonly applicationServer: ApplicationServer, - @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService ) { - StandaloneServices.initialize({ - [ICodeEditorService.toString()]: codeEditorService, - }); } protected async getModel(uri: URI, toDispose: DisposableCollection): Promise { - const reference = await this.textModelService.createModelReference(uri); + const reference = await StandaloneServices.get(ITextModelService).createModelReference(monaco.Uri.from(uri.toComponents())) as IReference; // if document is invalid makes sure that all events from underlying resource are processed before throwing invalid model if (!reference.object.valid) { await reference.object.sync(); @@ -139,34 +111,20 @@ export class MonacoEditorProvider { protected async doCreateEditor(uri: URI, factory: ( override: EditorServiceOverrides, toDispose: DisposableCollection) => Promise ): Promise { - const commandService = this.commandServiceFactory(); const domNode = document.createElement('div'); - const contextKeyService = this.contextKeyService.createScoped(domNode); - const { codeEditorService, textModelService, contextMenuService } = this; - const workspaceEditService = this.bulkEditService; - const toDispose = new DisposableCollection(commandService); - const openerService = new MonacoOpenerService(codeEditorService, commandService); - openerService.registerOpener({ + const contextKeyService = StandaloneServices.get(IContextKeyService).createScoped(domNode); + StandaloneServices.get(IOpenerService).registerOpener({ open: (u, options) => this.interceptOpen(u, options) }); const overrides: EditorServiceOverrides = [ - [ICodeEditorService, codeEditorService], - [ITextModelService, textModelService], - [IContextMenuService, contextMenuService], - [IBulkEditService, workspaceEditService], [IContextKeyService, contextKeyService], - [IOpenerService, openerService], - [IQuickInputService, this.quickInputService], - [ICommandService, commandService] ]; + const toDispose = new DisposableCollection(); const editor = await factory(overrides, toDispose); editor.onDispose(() => toDispose.dispose()); this.injectKeybindingResolver(editor); - const standaloneCommandService = new StandaloneCommandService(StandaloneServices.get(IInstantiationService)); - commandService.setDelegate(standaloneCommandService); - toDispose.push(editor.onFocusChanged(focused => { if (focused) { this._current = editor; @@ -212,16 +170,16 @@ export class MonacoEditorProvider { protected injectKeybindingResolver(editor: MonacoEditor): void { const keybindingService = StandaloneServices.get(IKeybindingService); - keybindingService.resolveKeybinding = keybinding => [new MonacoResolvedKeybinding(MonacoResolvedKeybinding.keySequence(keybinding), this.keybindingRegistry)]; + keybindingService.resolveKeybinding = keybinding => [new MonacoResolvedKeybinding(MonacoResolvedKeybinding.keySequence(keybinding.chords), this.keybindingRegistry)]; keybindingService.resolveKeyboardEvent = keyboardEvent => { - const keybinding = new SimpleKeybinding( + const keybinding = new KeyCodeChord( keyboardEvent.ctrlKey, keyboardEvent.shiftKey, keyboardEvent.altKey, keyboardEvent.metaKey, keyboardEvent.keyCode - ).toChord(); - return new MonacoResolvedKeybinding(MonacoResolvedKeybinding.keySequence(keybinding), this.keybindingRegistry); + ); + return new MonacoResolvedKeybinding(MonacoResolvedKeybinding.keySequence([keybinding]), this.keybindingRegistry); }; } @@ -248,13 +206,20 @@ export class MonacoEditorProvider { } })); toDispose.push(editor.onLanguageChanged(() => this.updateMonacoEditorOptions(editor))); + toDispose.push(editor.onDidChangeReadOnly(() => this.updateReadOnlyMessage(options, model.readOnly))); editor.document.onWillSaveModel(event => event.waitUntil(this.formatOnSave(editor, event))); return editor; } + + protected updateReadOnlyMessage(options: MonacoEditor.IOptions, readOnly: boolean | MarkdownString): void { + options.readOnlyMessage = MarkdownString.is(readOnly) ? readOnly : undefined; + } + protected createMonacoEditorOptions(model: MonacoEditorModel): MonacoEditor.IOptions { const options = this.createOptions(this.preferencePrefixes, model.uri, model.languageId); options.model = model.textEditorModel; options.readOnly = model.readOnly; + this.updateReadOnlyMessage(options, model.readOnly); options.lineNumbersMinChars = model.lineNumbersMinChars; return options; } @@ -336,6 +301,7 @@ export class MonacoEditorProvider { const options = this.createOptions(this.diffPreferencePrefixes, modified.uri, modified.languageId); options.originalEditable = !original.readOnly; options.readOnly = modified.readOnly; + options.readOnlyMessage = MarkdownString.is(modified.readOnly) ? modified.readOnly : undefined; return options; } protected updateMonacoDiffEditorOptions(editor: MonacoDiffEditor, event?: EditorPreferenceChange, resourceUri?: string): void { @@ -403,10 +369,10 @@ export class MonacoEditorProvider { const overrides = override ? Array.from(override) : []; overrides.push([IContextMenuService, { showContextMenu: () => {/** no op! */ } }]); const document = new MonacoEditorModel({ - uri, - readContents: async () => '', - dispose: () => { } - }, this.m2p, this.p2m); + uri, + readContents: async () => '', + dispose: () => { } + }, this.m2p, this.p2m); toDispose.push(document); const model = (await document.load()).textEditorModel; return new MonacoEditor( @@ -449,4 +415,41 @@ export class MonacoEditorProvider { } }; + async createEmbeddedDiffEditor(parentEditor: MonacoEditor, node: HTMLElement, originalUri: URI, modifiedUri: URI = parentEditor.uri, + options?: MonacoDiffEditor.IOptions): Promise { + options = { + scrollBeyondLastLine: true, + overviewRulerLanes: 2, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderSideBySide: false, + readOnly: true, + renderIndicators: false, + diffAlgorithm: 'advanced', + stickyScroll: { enabled: false }, + ...options, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + ...options?.scrollbar + } + }; + const uri = DiffUris.encode(originalUri, modifiedUri); + return await this.doCreateEditor(uri, async (override, toDispose) => + new MonacoDiffEditor( + uri, + node, + await this.getModel(originalUri, toDispose), + await this.getModel(modifiedUri, toDispose), + this.services, + this.diffNavigatorFactory, + options, + override, + parentEditor + ) + ) as MonacoDiffEditor; + } } diff --git a/packages/monaco/src/browser/monaco-editor-service.ts b/packages/monaco/src/browser/monaco-editor-service.ts index e6cee47621571..98d459616aeba 100644 --- a/packages/monaco/src/browser/monaco-editor-service.ts +++ b/packages/monaco/src/browser/monaco-editor-service.ts @@ -22,15 +22,20 @@ import { MonacoEditor } from './monaco-editor'; import { MonacoToProtocolConverter } from './monaco-to-protocol-converter'; import { MonacoEditorModel } from './monaco-editor-model'; import { IResourceEditorInput, ITextResourceEditorInput } from '@theia/monaco-editor-core/esm/vs/platform/editor/common/editor'; -import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; -import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme'; import { StandaloneCodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditorService'; import { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor'; import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; -import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService'; +import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; +import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; decorate(injectable(), StandaloneCodeEditorService); +export const VSCodeContextKeyService = Symbol('VSCodeContextKeyService'); +export const VSCodeThemeService = Symbol('VSCodeThemeService'); + +export const MonacoEditorServiceFactory = Symbol('MonacoEditorServiceFactory'); +export type MonacoEditorServiceFactoryType = (contextKeyService: IContextKeyService, themeService: IThemeService) => MonacoEditorService; + @injectable() export class MonacoEditorService extends StandaloneCodeEditorService { @@ -51,8 +56,8 @@ export class MonacoEditorService extends StandaloneCodeEditorService { @inject(PreferenceService) protected readonly preferencesService: PreferenceService; - constructor(@inject(VSCodeContextKeyService) contextKeyService: VSCodeContextKeyService) { - super(contextKeyService, StandaloneServices.get(IStandaloneThemeService)); + constructor(@inject(VSCodeContextKeyService) contextKeyService: IContextKeyService, @inject(VSCodeThemeService) themeService: IThemeService) { + super(contextKeyService, themeService); } /** @@ -62,7 +67,7 @@ export class MonacoEditorService extends StandaloneCodeEditorService { let editor = MonacoEditor.getCurrent(this.editors); if (!editor && CustomEditorWidget.is(this.shell.activeWidget)) { const model = this.shell.activeWidget.modelRef.object; - if (model.editorTextModel instanceof MonacoEditorModel) { + if (model?.editorTextModel instanceof MonacoEditorModel) { editor = MonacoEditor.findByDocument(this.editors, model.editorTextModel)[0]; } } @@ -138,6 +143,9 @@ export class MonacoEditorService extends StandaloneCodeEditorService { } const area = (ref && this.shell.getAreaFor(ref)) || 'main'; const mode = ref && sideBySide ? 'split-right' : undefined; + if (area === 'secondaryWindow') { + return { area: 'main', mode }; + } return { area, mode, ref }; } diff --git a/packages/monaco/src/browser/monaco-editor.ts b/packages/monaco/src/browser/monaco-editor.ts index 3bee47b20feff..069e5ae66b4cf 100644 --- a/packages/monaco/src/browser/monaco-editor.ts +++ b/packages/monaco/src/browser/monaco-editor.ts @@ -35,7 +35,8 @@ import { EditorDecoration, EditorMouseEvent, EncodingMode, - EditorDecorationOptions + EditorDecorationOptions, + MouseTargetType } from '@theia/editor/lib/browser'; import { MonacoEditorModel } from './monaco-editor-model'; import { MonacoToProtocolConverter } from './monaco-to-protocol-converter'; @@ -46,9 +47,22 @@ import * as monaco from '@theia/monaco-editor-core'; import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language'; import { IInstantiationService, ServiceIdentifier } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation'; -import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; +import { ICodeEditor, IMouseTargetMargin } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; +import { IStandaloneEditorConstructionOptions, StandaloneCodeEditor, StandaloneEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor'; import { ServiceCollection } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/serviceCollection'; -import { IStandaloneEditorConstructionOptions, StandaloneEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; +import { ConfigurationChangedEvent, IEditorOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions'; +import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; +import { ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands'; +import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; +import { IKeybindingService } from '@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybinding'; +import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { INotificationService } from '@theia/monaco-editor-core/esm/vs/platform/notification/common/notification'; +import { IAccessibilityService } from '@theia/monaco-editor-core/esm/vs/platform/accessibility/common/accessibility'; +import { ILanguageConfigurationService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures'; +import * as objects from '@theia/monaco-editor-core/esm/vs/base/common/objects'; +import { Selection } from '@theia/editor/lib/browser/editor'; export type ServicePair = [ServiceIdentifier, T]; @@ -81,10 +95,11 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { protected editor: monaco.editor.IStandaloneCodeEditor; protected readonly onCursorPositionChangedEmitter = new Emitter(); - protected readonly onSelectionChangedEmitter = new Emitter(); + protected readonly onSelectionChangedEmitter = new Emitter(); protected readonly onFocusChangedEmitter = new Emitter(); protected readonly onDocumentContentChangedEmitter = new Emitter(); protected readonly onMouseDownEmitter = new Emitter(); + readonly onDidChangeReadOnly = this.document.onDidChangeReadOnly; protected readonly onLanguageChangedEmitter = new Emitter(); readonly onLanguageChanged = this.onLanguageChangedEmitter.event; protected readonly onScrollChangedEmitter = new Emitter(); @@ -100,7 +115,8 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { readonly node: HTMLElement, services: MonacoEditorServices, options?: MonacoEditor.IOptions, - override?: EditorServiceOverrides + override?: EditorServiceOverrides, + readonly parentEditor?: MonacoEditor ) { super(services); this.toDispose.pushAll([ @@ -116,7 +132,10 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { this.autoSizing = options && options.autoSizing !== undefined ? options.autoSizing : false; this.minHeight = options && options.minHeight !== undefined ? options.minHeight : -1; this.maxHeight = options && options.maxHeight !== undefined ? options.maxHeight : -1; - this.toDispose.push(this.create(options, override)); + this.toDispose.push(this.create({ + ...MonacoEditor.createReadOnlyOptions(document.readOnly), + ...options + }, override)); this.addHandlers(this.editor); } @@ -147,11 +166,13 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { * @monaco-uplift. Should be guaranteed to work. * Incomparable enums prevent TypeScript from believing that public IStandaloneCodeEditor is satisfied by private StandaloneCodeEditor */ - return this.editor = instantiator.createInstance(StandaloneEditor, this.node, combinedOptions) as unknown as monaco.editor.IStandaloneCodeEditor; + return this.editor = (this.parentEditor ? + instantiator.createInstance(EmbeddedCodeEditor, this.node, combinedOptions, this.parentEditor.getControl() as unknown as ICodeEditor) : + instantiator.createInstance(StandaloneEditor, this.node, combinedOptions)) as unknown as monaco.editor.IStandaloneCodeEditor; } protected getInstantiatorWithOverrides(override?: EditorServiceOverrides): IInstantiationService { - const instantiator = StandaloneServices.initialize({}); + const instantiator = StandaloneServices.get(IInstantiationService); if (override) { const overrideServices = new ServiceCollection(...override); return instantiator.createChild(overrideServices); @@ -172,8 +193,11 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { this.toDispose.push(codeEditor.onDidChangeCursorPosition(() => this.onCursorPositionChangedEmitter.fire(this.cursor) )); - this.toDispose.push(codeEditor.onDidChangeCursorSelection(() => - this.onSelectionChangedEmitter.fire(this.selection) + this.toDispose.push(codeEditor.onDidChangeCursorSelection(event => + this.onSelectionChangedEmitter.fire({ + ...this.m2p.asRange(event.selection), + direction: event.selection.getDirection() === monaco.SelectionDirection.LTR ? 'ltr' : 'rtl' + }) )); this.toDispose.push(codeEditor.onDidFocusEditorText(() => this.onFocusChangedEmitter.fire(this.isFocused()) @@ -185,12 +209,12 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { const { element, position, range } = e.target; this.onMouseDownEmitter.fire({ target: { - ...e.target, + type: e.target.type as unknown as MouseTargetType, element: element || undefined, mouseColumn: this.m2p.asPosition(undefined, e.target.mouseColumn).character, range: range && this.m2p.asRange(range) || undefined, position: position && this.m2p.asPosition(position.lineNumber, position.column) || undefined, - detail: (e.target as monaco.editor.IMouseTargetMargin).detail || {}, + detail: (e.target as unknown as IMouseTargetMargin).detail || {}, }, event: e.event.browserEvent }); @@ -198,6 +222,9 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { this.toDispose.push(codeEditor.onDidScrollChange(e => { this.onScrollChangedEmitter.fire(undefined); })); + this.toDispose.push(this.onDidChangeReadOnly(readOnly => { + codeEditor.updateOptions(MonacoEditor.createReadOnlyOptions(readOnly)); + })); } getVisibleRanges(): Range[] { @@ -220,8 +247,8 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { return this.onDocumentContentChangedEmitter.event; } - get isReadonly(): boolean { - return this.document.isReadonly(); + get isReadonly(): boolean | MarkdownString { + return this.document.readOnly; } get cursor(): Position { @@ -238,16 +265,16 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { return this.onCursorPositionChangedEmitter.event; } - get selection(): Range { - return this.m2p.asRange(this.editor.getSelection()!); + get selection(): Selection { + return this.m2p.asSelection(this.editor.getSelection()!); } - set selection(selection: Range) { + set selection(selection: Selection) { const range = this.p2m.asRange(selection); this.editor.setSelection(range); } - get onSelectionChanged(): Event { + get onSelectionChanged(): Event { return this.onSelectionChangedEmitter.event; } @@ -580,6 +607,9 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { return this.uri.withPath(resourceUri.path); } + shouldDisplayDirtyDiff(): boolean { + return true; + } } export namespace MonacoEditor { @@ -641,4 +671,63 @@ export namespace MonacoEditor { return candidate && candidate.getControl() === control; }); } + + export function createReadOnlyOptions(readOnly?: boolean | MarkdownString): monaco.editor.IEditorOptions { + if (typeof readOnly === 'boolean') { + return { readOnly, readOnlyMessage: undefined }; + } + if (readOnly) { + return { readOnly: true, readOnlyMessage: readOnly }; + } + return {}; + } +} + +// adapted from https://github.com/microsoft/vscode/blob/0bd70d48ad8b3e2fb1922aa54f87c786ff2b4bd8/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts +// This class reproduces the logic in EmbeddedCodeEditorWidget but extends StandaloneCodeEditor rather than CodeEditorWidget. +class EmbeddedCodeEditor extends StandaloneCodeEditor { + + private readonly _parentEditor: ICodeEditor; + private readonly _overwriteOptions: IEditorOptions; + + constructor( + domElement: HTMLElement, + options: Readonly, + parentEditor: ICodeEditor, + @IInstantiationService instantiationService: IInstantiationService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @ICommandService commandService: ICommandService, + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @IThemeService themeService: IThemeService, + @INotificationService notificationService: INotificationService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + ) { + super(domElement, { ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() }, instantiationService, codeEditorService, + commandService, contextKeyService, keybindingService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); + + this._parentEditor = parentEditor; + this._overwriteOptions = options; + + // Overwrite parent's options + super.updateOptions(this._overwriteOptions); + + this._register(parentEditor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => this._onParentConfigurationChanged(e))); + } + + getParentEditor(): ICodeEditor { + return this._parentEditor; + } + + private _onParentConfigurationChanged(e: ConfigurationChangedEvent): void { + super.updateOptions(this._parentEditor.getRawOptions()); + super.updateOptions(this._overwriteOptions); + } + + override updateOptions(newOptions: IEditorOptions): void { + objects.mixin(this._overwriteOptions, newOptions, true); + super.updateOptions(this._overwriteOptions); + } } diff --git a/packages/monaco/src/browser/monaco-formatting-conflicts.ts b/packages/monaco/src/browser/monaco-formatting-conflicts.ts index 361db7e5350cd..b3eb98faf823f 100644 --- a/packages/monaco/src/browser/monaco-formatting-conflicts.ts +++ b/packages/monaco/src/browser/monaco-formatting-conflicts.ts @@ -59,13 +59,13 @@ export class MonacoFormattingConflictsContribution implements FrontendApplicatio await this.preferenceService.set(name, formatter); } - private getDefaultFormatter(language: string): string | undefined { + private getDefaultFormatter(language: string, resourceURI: string): string | undefined { const name = this.preferenceSchema.overridePreferenceName({ preferenceName: PREFERENCE_NAME, overrideIdentifier: language }); - return this.preferenceService.get(name); + return this.preferenceService.get(name, undefined, resourceURI); } private async selectFormatter( @@ -85,7 +85,7 @@ export class MonacoFormattingConflictsContribution implements FrontendApplicatio } const languageId = currentEditor.editor.document.languageId; - const defaultFormatterId = this.getDefaultFormatter(languageId); + const defaultFormatterId = this.getDefaultFormatter(languageId, document.uri.toString()); if (defaultFormatterId) { const formatter = formatters.find(f => f.extensionId && f.extensionId.value === defaultFormatterId); diff --git a/packages/monaco/src/browser/monaco-frontend-application-contribution.ts b/packages/monaco/src/browser/monaco-frontend-application-contribution.ts index 4d8ccb9511129..e93e9700b8295 100644 --- a/packages/monaco/src/browser/monaco-frontend-application-contribution.ts +++ b/packages/monaco/src/browser/monaco-frontend-application-contribution.ts @@ -20,44 +20,26 @@ import { MonacoSnippetSuggestProvider } from './monaco-snippet-suggest-provider' import * as monaco from '@theia/monaco-editor-core'; import { setSnippetSuggestSupport } from '@theia/monaco-editor-core/esm/vs/editor/contrib/suggest/browser/suggest'; import { CompletionItemProvider } from '@theia/monaco-editor-core/esm/vs/editor/common/languages'; -import { MonacoEditorService } from './monaco-editor-service'; import { MonacoTextModelService } from './monaco-text-model-service'; -import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService'; -import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; -import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; -import { ITextModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService'; -import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; -import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView'; -import { MonacoContextMenuService } from './monaco-context-menu'; import { MonacoThemingService } from './monaco-theming-service'; import { isHighContrast } from '@theia/core/lib/common/theme'; import { editorOptionsRegistry, IEditorOption } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions'; import { MAX_SAFE_INTEGER } from '@theia/core'; import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/browser/editor-generated-preference-schema'; import { WorkspaceFileService } from '@theia/workspace/lib/common/workspace-file-service'; - -let theiaDidInitialize = false; -const originalInitialize = StandaloneServices.initialize; -StandaloneServices.initialize = overrides => { - if (!theiaDidInitialize) { - console.warn('Monaco was initialized before overrides were installed by Theia\'s initialization.' - + ' Please check the lifecycle of services that use Monaco and ensure that Monaco entities are not instantiated before Theia is initialized.', new Error()); - } - return originalInitialize(overrides); -}; +import { SecondaryWindowHandler } from '@theia/core/lib/browser/secondary-window-handler'; +import { EditorWidget } from '@theia/editor/lib/browser'; +import { MonacoEditor } from './monaco-editor'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { StandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneThemeService'; +import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme'; @injectable() export class MonacoFrontendApplicationContribution implements FrontendApplicationContribution, StylingParticipant { - @inject(MonacoEditorService) - protected readonly codeEditorService: MonacoEditorService; - @inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService; - @inject(VSCodeContextKeyService) - protected readonly contextKeyService: VSCodeContextKeyService; - @inject(MonacoSnippetSuggestProvider) protected readonly snippetSuggestProvider: MonacoSnippetSuggestProvider; @@ -67,24 +49,16 @@ export class MonacoFrontendApplicationContribution implements FrontendApplicatio @inject(QuickAccessRegistry) protected readonly quickAccessRegistry: QuickAccessRegistry; - @inject(MonacoContextMenuService) - protected readonly contextMenuService: MonacoContextMenuService; - @inject(MonacoThemingService) protected readonly monacoThemingService: MonacoThemingService; @inject(WorkspaceFileService) protected readonly workspaceFileService: WorkspaceFileService; + @inject(SecondaryWindowHandler) + protected readonly secondaryWindowHandler: SecondaryWindowHandler; + @postConstruct() protected init(): void { this.addAdditionalPreferenceValidations(); - const { codeEditorService, textModelService, contextKeyService, contextMenuService } = this; - theiaDidInitialize = true; - StandaloneServices.initialize({ - [ICodeEditorService.toString()]: codeEditorService, - [ITextModelService.toString()]: textModelService, - [IContextKeyService.toString()]: contextKeyService, - [IContextMenuService.toString()]: contextMenuService, - }); // Monaco registers certain quick access providers (e.g. QuickCommandAccess) at import time, but we want to use our own. this.quickAccessRegistry.clear(); @@ -117,6 +91,14 @@ export class MonacoFrontendApplicationContribution implements FrontendApplicatio 'extensions': workspaceExtensions.map(ext => `.${ext}`) }); } + onStart(): void { + this.secondaryWindowHandler.onDidAddWidget(([widget, window]) => { + if (widget instanceof EditorWidget && widget.editor instanceof MonacoEditor) { + const themeService = StandaloneServices.get(IStandaloneThemeService) as StandaloneThemeService; + themeService.registerEditorContainer(widget.node); + } + }); + } registerThemeStyle(theme: ColorTheme, collector: CssStyleCollector): void { if (isHighContrast(theme.type)) { diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index 6a717c3f4dab5..e7d727453bb07 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -14,29 +14,15 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import * as MonacoNls from '@theia/monaco-editor-core/esm/vs/nls'; -import { nls } from '@theia/core/lib/common/nls'; -import { FormatType, Localization } from '@theia/core/lib/common/i18n/localization'; - -Object.assign(MonacoNls, { - localize(_key: string, label: string, ...args: FormatType[]): string { - if (nls.locale) { - const defaultKey = nls.getDefaultKey(label); - if (defaultKey) { - return nls.localize(defaultKey, label, ...args); - } - } - return Localization.format(label, args); - } -}); - import '../../src/browser/style/index.css'; -import { ContainerModule, decorate, injectable, interfaces } from '@theia/core/shared/inversify'; +import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; import { MenuContribution, CommandContribution, quickInputServicePath } from '@theia/core/lib/common'; import { FrontendApplicationContribution, KeybindingContribution, PreferenceService, PreferenceSchemaProvider, createPreferenceProxy, - PreferenceScope, PreferenceChange, OVERRIDE_PROPERTY_PATTERN, QuickInputService, StylingParticipant, WebSocketConnectionProvider + PreferenceScope, PreferenceChange, OVERRIDE_PROPERTY_PATTERN, QuickInputService, StylingParticipant, WebSocketConnectionProvider, + UndoRedoHandler, + WidgetStatusBarContribution } from '@theia/core/lib/browser'; import { TextEditorProvider, DiffNavigatorProvider, TextEditor } from '@theia/editor/lib/browser'; import { MonacoEditorProvider, MonacoEditorFactory } from './monaco-editor-provider'; @@ -45,12 +31,12 @@ import { MonacoEditorCommandHandlers } from './monaco-command'; import { MonacoKeybindingContribution } from './monaco-keybinding'; import { MonacoLanguages } from './monaco-languages'; import { MonacoWorkspace } from './monaco-workspace'; -import { MonacoEditorService } from './monaco-editor-service'; +import { MonacoEditorService, MonacoEditorServiceFactory, VSCodeContextKeyService, VSCodeThemeService } from './monaco-editor-service'; import { MonacoTextModelService, MonacoEditorModelFactory } from './monaco-text-model-service'; import { MonacoContextMenuService } from './monaco-context-menu'; import { MonacoOutlineContribution } from './monaco-outline-contribution'; import { MonacoStatusBarContribution } from './monaco-status-bar-contribution'; -import { MonacoCommandService, MonacoCommandServiceFactory } from './monaco-command-service'; +import { MonacoCommandService } from './monaco-command-service'; import { MonacoCommandRegistry } from './monaco-command-registry'; import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory'; import { MonacoFrontendApplicationContribution } from './monaco-frontend-application-contribution'; @@ -80,16 +66,16 @@ import { GotoLineQuickAccessContribution } from './monaco-gotoline-quick-access' import { GotoSymbolQuickAccessContribution } from './monaco-gotosymbol-quick-access'; import { QuickAccessContribution, QuickAccessRegistry } from '@theia/core/lib/browser/quick-input/quick-access'; import { MonacoQuickAccessRegistry } from './monaco-quick-access-registry'; -import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService'; import { ConfigurationTarget, IConfigurationChangeEvent, IConfigurationService } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configuration'; -import { StandaloneConfigurationService, StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { StandaloneConfigurationService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { Configuration } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configurationModels'; import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; import { MonacoMarkdownRenderer } from './markdown-renderer/monaco-markdown-renderer'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { ThemeServiceWithDB } from './monaco-indexed-db'; - -decorate(injectable(), VSCodeContextKeyService); +import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; +import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { ActiveMonacoUndoRedoHandler, FocusedMonacoUndoRedoHandler } from './monaco-undo-redo-handler'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoThemingService).toSelf().inSingletonScope(); @@ -114,15 +100,16 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoWorkspace).toSelf().inSingletonScope(); - bind(MonacoConfigurationService).toDynamicValue(({ container }) => - createMonacoConfigurationService(container) - ).inSingletonScope(); - bind(VSCodeContextKeyService).toDynamicValue(({ container }) => - new VSCodeContextKeyService(container.get(MonacoConfigurationService)) - ).inSingletonScope(); + bind(MonacoConfigurationService).toDynamicValue(({ container }) => createMonacoConfigurationService(container)).inSingletonScope(); bind(MonacoBulkEditService).toSelf().inSingletonScope(); - bind(MonacoEditorService).toSelf().inSingletonScope(); + bind(MonacoEditorServiceFactory).toFactory((context: interfaces.Context) => (contextKeyService: IContextKeyService, themeService: IThemeService) => { + const child = context.container.createChild(); + child.bind(VSCodeContextKeyService).toConstantValue(contextKeyService); + child.bind(VSCodeThemeService).toConstantValue(themeService); + child.bind(MonacoEditorService).toSelf().inSingletonScope(); + return child.get(MonacoEditorService); + }); bind(MonacoTextModelService).toSelf().inSingletonScope(); bind(MonacoContextMenuService).toSelf().inSingletonScope(); bind(MonacoEditorServices).toSelf().inSingletonScope(); @@ -130,7 +117,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bindContributionProvider(bind, MonacoEditorFactory); bindContributionProvider(bind, MonacoEditorModelFactory); bind(MonacoCommandService).toSelf().inTransientScope(); - bind(MonacoCommandServiceFactory).toAutoFactory(MonacoCommandService); bind(TextEditorProvider).toProvider(context => uri => context.container.get(MonacoEditorProvider).get(uri) @@ -150,7 +136,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(FrontendApplicationContribution).toService(MonacoFormattingConflictsContribution); bind(MonacoStatusBarContribution).toSelf().inSingletonScope(); - bind(FrontendApplicationContribution).toService(MonacoStatusBarContribution); + bind(WidgetStatusBarContribution).toService(MonacoStatusBarContribution); bind(MonacoCommandRegistry).toSelf().inSingletonScope(); bind(MonacoEditorCommandHandlers).toSelf().inSingletonScope(); @@ -192,13 +178,18 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoIconRegistry).toSelf().inSingletonScope(); bind(IconRegistry).toService(MonacoIconRegistry); + + bind(FocusedMonacoUndoRedoHandler).toSelf().inSingletonScope(); + bind(ActiveMonacoUndoRedoHandler).toSelf().inSingletonScope(); + bind(UndoRedoHandler).toService(FocusedMonacoUndoRedoHandler); + bind(UndoRedoHandler).toService(ActiveMonacoUndoRedoHandler); }); export const MonacoConfigurationService = Symbol('MonacoConfigurationService'); export function createMonacoConfigurationService(container: interfaces.Container): IConfigurationService { const preferences = container.get(PreferenceService); const preferenceSchemaProvider = container.get(PreferenceSchemaProvider); - const service = StandaloneServices.get(IConfigurationService) as StandaloneConfigurationService; + const service = new StandaloneConfigurationService(); const _configuration: Configuration = service['_configuration']; _configuration.getValue = (section, overrides) => { @@ -216,6 +207,14 @@ export function createMonacoConfigurationService(container: interfaces.Container return proxy; }; + /* + * Since we never read values from the underlying service, writing to it doesn't make sense. The standalone editor writes to the configuration when being created, + * which makes sense in the standalone case where there is no preference infrastructure in place. Those writes degrade the performance, however, so we patch the + * service to an empty implementation. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + service.updateValues = (values: [string, any][]) => Promise.resolve(); + const toTarget = (scope: PreferenceScope): ConfigurationTarget => { switch (scope) { case PreferenceScope.Default: return ConfigurationTarget.DEFAULT; @@ -246,11 +245,12 @@ export function createMonacoConfigurationService(container: interfaces.Container overrides.push([override, [...values]]); } service['_onDidChangeConfiguration'].fire({ + sourceConfig: {}, change: { keys: [...context.keys], overrides }, - affectedKeys: [...context.affectedKeys], + affectedKeys: context.affectedKeys, source, affectsConfiguration: (prefix, options) => { if (!context.affectedKeys.has(prefix)) { diff --git a/packages/monaco/src/browser/monaco-icon-registry.ts b/packages/monaco/src/browser/monaco-icon-registry.ts index 48cadadcdf118..0df38bf51aa25 100644 --- a/packages/monaco/src/browser/monaco-icon-registry.ts +++ b/packages/monaco/src/browser/monaco-icon-registry.ts @@ -15,9 +15,9 @@ // ***************************************************************************** import { injectable } from '@theia/core/shared/inversify'; -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; import { IconRegistry } from '@theia/core/lib/browser/icon-registry'; -import { IconDefinition, IconFontDefinition, getIconRegistry } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/iconRegistry'; +import { getIconRegistry } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/iconRegistry'; +import { IconDefinition, IconFontDefinition, ThemeIcon } from '@theia/core/lib/common/theme'; @injectable() export class MonacoIconRegistry implements IconRegistry { @@ -33,7 +33,8 @@ export class MonacoIconRegistry implements IconRegistry { } registerIconFont(id: string, definition: IconFontDefinition): IconFontDefinition { - return this.iconRegistry.registerIconFont(id, definition); + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + return this.iconRegistry.registerIconFont(id, definition) as IconFontDefinition; } deregisterIconFont(id: string): void { @@ -41,7 +42,8 @@ export class MonacoIconRegistry implements IconRegistry { } getIconFont(id: string): IconFontDefinition | undefined { - return this.iconRegistry.getIconFont(id); + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + return this.iconRegistry.getIconFont(id) as IconFontDefinition; } } diff --git a/packages/monaco/src/browser/monaco-init.ts b/packages/monaco/src/browser/monaco-init.ts new file mode 100644 index 0000000000000..d622e596c3df5 --- /dev/null +++ b/packages/monaco/src/browser/monaco-init.ts @@ -0,0 +1,134 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 +// ***************************************************************************** + +/* + * The code in this file is responsible for overriding service implementations in the Monaco editor with our own Theia-based implementations. + * Since we only get a single chance to call `StandaloneServies.initialize()` with our overrides, we need to make sure that intialize is called before the first call to + * `StandaloneServices.get()` or `StandaloneServies.initialize()`. As we do not control the mechanics of Inversify instance constructions, the approach here is to call + * `MonacoInit.init()` from the `index.js` file after all container modules are loaded, but before the first object is fetched from it. + * `StandaloneServices.initialize()` is called with service descriptors, not service instances. This lets us finish all overrides before any inversify object is constructed and + * might call `initialize()` while being constructed. + * The service descriptors require a constructor function, so we declare dummy class for each Monaco service we override. But instead of returning an instance of the dummy class, + * we fetch the implementation of the monaco service from the inversify container. + * The inversify-constructed services must not call StandaloneServices.get() or StandaloneServices.initialize() from their constructors. Calling `get`()` in postConstruct mehtods + * is allowed. + */ + +// Before importing anything from monaco we need to override its localization function +import * as MonacoNls from '@theia/monaco-editor-core/esm/vs/nls'; +import { nls } from '@theia/core/lib/common/nls'; +import { FormatType, Localization } from '@theia/core/lib/common/i18n/localization'; + +Object.assign(MonacoNls, { + localize(_key: string, label: string, ...args: FormatType[]): string { + if (nls.locale) { + const defaultKey = nls.getDefaultKey(label); + if (defaultKey) { + return nls.localize(defaultKey, label, ...args); + } + } + return Localization.format(label, args); + } +}); + +import { Container } from '@theia/core/shared/inversify'; +import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { SyncDescriptor } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/descriptors'; +import { MonacoEditorServiceFactory, MonacoEditorServiceFactoryType } from './monaco-editor-service'; +import { IConfigurationService } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configuration'; +import { ITextModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService'; +import { MonacoConfigurationService } from './monaco-frontend-module'; +import { MonacoTextModelService } from './monaco-text-model-service'; +import { MonacoContextMenuService } from './monaco-context-menu'; +import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView'; +import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; +import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { MonacoBulkEditService } from './monaco-bulk-edit-service'; +import { MonacoCommandService } from './monaco-command-service'; +import { IBulkEditService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService'; +import { ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands'; +import { MonacoQuickInputImplementation } from './monaco-quick-input-service'; +import { IQuickInputService } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickInput'; +import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme'; +import { MonacoStandaloneThemeService } from './monaco-standalone-theme-service'; + +class MonacoEditorServiceConstructor { + /** + * MonacoEditorService needs other Monaco services as constructor parameters, so we need to do use a factory for constructing the service. If we want the singleton instance, + * we need to fetch it from the `StandaloneServices` class instead of injecting it. + * @param container + * @param contextKeyService + * @param themeService + */ + constructor(container: Container, + @IContextKeyService contextKeyService: IContextKeyService, + @IThemeService themeService: IThemeService) { + + return container.get(MonacoEditorServiceFactory)(contextKeyService, themeService); + }; +} + +class MonacoConfigurationServiceConstructor { + constructor(container: Container) { + return container.get(MonacoConfigurationService); + } +} + +class MonacoTextModelServiceConstructor { + constructor(container: Container) { + return container.get(MonacoTextModelService); + } +} + +class MonacoContextMenuServiceConstructor { + constructor(container: Container) { + return container.get(MonacoContextMenuService); + } +} + +class MonacoBulkEditServiceConstructor { + constructor(container: Container) { + return container.get(MonacoBulkEditService); + } +} + +class MonacoCommandServiceConstructor { + constructor(container: Container) { + return container.get(MonacoCommandService); + } +} + +class MonacoQuickInputImplementationConstructor { + constructor(container: Container) { + return container.get(MonacoQuickInputImplementation); + } +} + +export namespace MonacoInit { + export function init(container: Container): void { + StandaloneServices.initialize({ + [ICodeEditorService.toString()]: new SyncDescriptor(MonacoEditorServiceConstructor, [container]), + [IConfigurationService.toString()]: new SyncDescriptor(MonacoConfigurationServiceConstructor, [container]), + [ITextModelService.toString()]: new SyncDescriptor(MonacoTextModelServiceConstructor, [container]), + [IContextMenuService.toString()]: new SyncDescriptor(MonacoContextMenuServiceConstructor, [container]), + [IBulkEditService.toString()]: new SyncDescriptor(MonacoBulkEditServiceConstructor, [container]), + [ICommandService.toString()]: new SyncDescriptor(MonacoCommandServiceConstructor, [container]), + [IQuickInputService.toString()]: new SyncDescriptor(MonacoQuickInputImplementationConstructor, [container]), + [IStandaloneThemeService.toString()]: new MonacoStandaloneThemeService() + }); + } +} diff --git a/packages/monaco/src/browser/monaco-keybinding.ts b/packages/monaco/src/browser/monaco-keybinding.ts index ec7e0bcf902d1..cc80a06470d09 100644 --- a/packages/monaco/src/browser/monaco-keybinding.ts +++ b/packages/monaco/src/browser/monaco-keybinding.ts @@ -45,14 +45,14 @@ export class MonacoKeybindingContribution implements KeybindingContribution { registerKeybindings(registry: KeybindingRegistry): void { const defaultKeybindings = KeybindingsRegistry.getDefaultKeybindings(); for (const item of defaultKeybindings) { - const command = this.commands.validate(item.command); - if (command) { + const command = this.commands.validate(item.command || undefined); + if (command && item.keybinding) { const when = (item.when && item.when.serialize()) ?? undefined; let keybinding; if (item.command === MonacoCommands.GO_TO_DEFINITION && !environment.electron.is()) { keybinding = 'ctrlcmd+f11'; } else { - keybinding = MonacoResolvedKeybinding.toKeybinding(item.keybinding); + keybinding = MonacoResolvedKeybinding.toKeybinding(item.keybinding.chords); } registry.registerKeybinding({ command, keybinding, when }); } diff --git a/packages/monaco/src/browser/monaco-menu.ts b/packages/monaco/src/browser/monaco-menu.ts index 40ab614701f43..21cebc0d42af1 100644 --- a/packages/monaco/src/browser/monaco-menu.ts +++ b/packages/monaco/src/browser/monaco-menu.ts @@ -14,12 +14,13 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject } from '@theia/core/shared/inversify'; -import { MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath, MenuAction } from '@theia/core/lib/common'; -import { EditorMainMenu, EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser'; -import { MonacoCommandRegistry } from './monaco-command-registry'; +import { MAIN_MENU_BAR, MenuAction, MenuContribution, MenuModelRegistry, MenuPath } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common/nls'; -import { IMenuItem, isIMenuItem, MenuId, MenuRegistry } from '@theia/monaco-editor-core/esm/vs/platform/actions/common/actions'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { EDITOR_CONTEXT_MENU, EditorMainMenu } from '@theia/editor/lib/browser'; +import { IMenuItem, MenuId, MenuRegistry, isIMenuItem } from '@theia/monaco-editor-core/esm/vs/platform/actions/common/actions'; +import { MonacoCommands } from './monaco-command'; +import { MonacoCommandRegistry } from './monaco-command-registry'; export interface MonacoActionGroup { id: string; @@ -46,7 +47,11 @@ export class MonacoEditorMenuContribution implements MenuContribution { const commandId = this.commands.validate(item.command.id); if (commandId) { const menuPath = [...EDITOR_CONTEXT_MENU, (item.group || '')]; - registry.registerMenuAction(menuPath, this.buildMenuAction(commandId, item)); + const coreId = MonacoCommands.COMMON_ACTIONS.get(commandId); + if (!(coreId && registry.getMenu(menuPath).children.some(it => it.id === coreId))) { + // Don't add additional actions if the item is already registered with a core ID. + registry.registerMenuAction(menuPath, this.buildMenuAction(commandId, item)); + } } } diff --git a/packages/monaco/src/browser/monaco-quick-input-service.ts b/packages/monaco/src/browser/monaco-quick-input-service.ts index 408d3c09aafb8..a611b6fe33e21 100644 --- a/packages/monaco/src/browser/monaco-quick-input-service.ts +++ b/packages/monaco/src/browser/monaco-quick-input-service.ts @@ -16,21 +16,21 @@ import { ApplicationShell, - InputBox, InputOptions, KeybindingRegistry, NormalizedQuickInputButton, PickOptions, + InputBox, InputOptions, KeybindingRegistry, PickOptions, QuickInputButton, QuickInputHideReason, QuickInputService, QuickPick, QuickPickItem, - QuickPickItemButtonEvent, QuickPickItemHighlights, QuickPickOptions, QuickPickSeparator, codiconArray + QuickPickItemButtonEvent, QuickPickItemHighlights, QuickPickOptions, QuickPickSeparator } from '@theia/core/lib/browser'; import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, - IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, QuickPickInput + IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickWidget, QuickPickInput } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickInput'; -import { IQuickInputOptions, IQuickInputStyles, QuickInputController } from '@theia/monaco-editor-core/esm/vs/base/parts/quickinput/browser/quickInput'; +import { IQuickInputOptions, IQuickInputStyles } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/browser/quickInput'; +import { QuickInputController } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/browser/quickInputController'; import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding'; import { IQuickAccessController } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickAccess'; import { QuickAccessController } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/browser/quickAccess'; -import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService'; -import { IContextKey } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; import { IListOptions, List } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/list/listWidget'; import * as monaco from '@theia/monaco-editor-core'; import { ResolvedKeybinding } from '@theia/monaco-editor-core/esm/vs/base/common/keybindings'; @@ -38,11 +38,10 @@ import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { IMatch } from '@theia/monaco-editor-core/esm/vs/base/common/filters'; import { IListRenderer, IListVirtualDelegate } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/list/list'; -import { Event } from '@theia/core'; +import { CancellationToken, Event } from '@theia/core'; import { MonacoColorRegistry } from './monaco-color-registry'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme'; -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; // Copied from @vscode/src/vs/base/parts/quickInput/browser/quickInputList.ts export interface IListElement { @@ -62,6 +61,7 @@ export interface IListElement { @injectable() export class MonacoQuickInputImplementation implements IQuickInputService { + declare readonly _serviceBrand: undefined; controller: QuickInputController; @@ -76,9 +76,6 @@ export class MonacoQuickInputImplementation implements IQuickInputService { @inject(ThemeService) protected readonly themeService: ThemeService; - @inject(VSCodeContextKeyService) - protected readonly contextKeyService: VSCodeContextKeyService; - protected container: HTMLElement; private quickInputList: List; @@ -93,25 +90,29 @@ export class MonacoQuickInputImplementation implements IQuickInputService { this.initContainer(); this.initController(); this.quickAccess = new QuickAccessController(this, StandaloneServices.get(IInstantiationService)); - this.inQuickOpen = this.contextKeyService.createKey('inQuickOpen', false); + this.inQuickOpen = StandaloneServices.get(IContextKeyService).createKey('inQuickOpen', false); this.controller.onShow(() => { this.container.style.top = this.shell.mainPanel.node.getBoundingClientRect().top + 'px'; this.inQuickOpen.set(true); }); this.controller.onHide(() => this.inQuickOpen.set(false)); - this.themeService.initialized.then(() => this.controller.applyStyles(this.getStyles())); + this.themeService.initialized.then(() => this.controller.applyStyles(this.computeStyles())); // Hook into the theming service of Monaco to ensure that the updates are ready. - StandaloneServices.get(IStandaloneThemeService).onDidColorThemeChange(() => this.controller.applyStyles(this.getStyles())); + StandaloneServices.get(IStandaloneThemeService).onDidColorThemeChange(() => this.controller.applyStyles(this.computeStyles())); window.addEventListener('resize', () => this.updateLayout()); } setContextKey(key: string | undefined): void { if (key) { - this.contextKeyService.createKey(key, undefined); + StandaloneServices.get(IContextKeyService).createKey(key, undefined); } } + createQuickWidget(): IQuickWidget { + return this.controller.createQuickWidget(); + } + createQuickPick(): IQuickPick { return this.controller.createQuickPick(); } @@ -184,7 +185,7 @@ export class MonacoQuickInputImplementation implements IQuickInputService { } private initController(): void { - this.controller = new QuickInputController(this.getOptions()); + this.controller = new QuickInputController(this.getOptions(), StandaloneServices.get(IStandaloneThemeService)); this.updateLayout(); } @@ -202,72 +203,107 @@ export class MonacoQuickInputImplementation implements IQuickInputService { const options: IQuickInputOptions = { idPrefix: 'quickInput_', container: this.container, - styles: { widget: {}, list: {}, inputBox: {}, countBadge: {}, button: {}, progressBar: {}, keybindingLabel: {}, }, + styles: this.computeStyles(), ignoreFocusOut: () => false, - isScreenReaderOptimized: () => false, // TODO change to true once support is added. backKeybindingLabel: () => undefined, setContextKey: (id?: string) => this.setContextKey(id), returnFocus: () => this.container.focus(), createList: ( user: string, container: HTMLElement, delegate: IListVirtualDelegate, renderers: IListRenderer[], listOptions: IListOptions ): List => this.quickInputList = new List(user, container, delegate, renderers, listOptions), + linkOpenerDelegate: () => { + // @monaco-uplift: not sure what to do here + } }; return options; } // @monaco-uplift // Keep the styles up to date with https://github.com/microsoft/vscode/blob/7888ff3a6b104e9e2e3d0f7890ca92dd0828215f/src/vs/platform/quickinput/browser/quickInput.ts#L171. - private getStyles(): IQuickInputStyles { + private computeStyles(): IQuickInputStyles { return { + toggle: { + inputActiveOptionBorder: this.colorRegistry.toCssVariableName('inputOption.activeBorder'), + inputActiveOptionForeground: this.colorRegistry.toCssVariableName('inputOption.activeForeground'), + inputActiveOptionBackground: this.colorRegistry.toCssVariableName('inputOption.activeBackground') + }, + pickerGroup: { + pickerGroupBorder: this.colorRegistry.toCssVariableName('pickerGroup.Border'), + pickerGroupForeground: this.colorRegistry.toCssVariableName('pickerGroupForeground') + }, widget: { - quickInputBackground: this.colorRegistry.getColor('quickInput.background'), - quickInputForeground: this.colorRegistry.getColor('quickInput.foreground'), - quickInputTitleBackground: this.colorRegistry.getColor('quickInputTitle.background') + quickInputBackground: this.colorRegistry.toCssVariableName('quickInput.background'), + quickInputForeground: this.colorRegistry.toCssVariableName('quickInput.foreground'), + quickInputTitleBackground: this.colorRegistry.toCssVariableName('quickInputTitle.background'), + widgetBorder: this.colorRegistry.toCssVariableName('widget.border'), + widgetShadow: this.colorRegistry.toCssVariableName('widget.shadow') }, list: { - listBackground: this.colorRegistry.getColor('quickInput.background'), - listInactiveFocusForeground: this.colorRegistry.getColor('quickInputList.focusForeground'), - listInactiveSelectionIconForeground: this.colorRegistry.getColor('quickInputList.focusIconForeground'), - listInactiveFocusBackground: this.colorRegistry.getColor('quickInputList.focusBackground'), - listFocusOutline: this.colorRegistry.getColor('activeContrastBorder'), - listInactiveFocusOutline: this.colorRegistry.getColor('activeContrastBorder'), - pickerGroupBorder: this.colorRegistry.getColor('pickerGroup.border'), - pickerGroupForeground: this.colorRegistry.getColor('pickerGroup.foreground') + listBackground: this.colorRegistry.toCssVariableName('quickInput.background'), + listInactiveFocusForeground: this.colorRegistry.toCssVariableName('quickInputList.focusForeground'), + listInactiveSelectionIconForeground: this.colorRegistry.toCssVariableName('quickInputList.focusIconForeground'), + listInactiveFocusBackground: this.colorRegistry.toCssVariableName('quickInputList.focusBackground'), + listFocusOutline: this.colorRegistry.toCssVariableName('activeContrastBorder'), + listInactiveFocusOutline: this.colorRegistry.toCssVariableName('activeContrastBorder'), + + listFocusBackground: this.colorRegistry.toCssVariableName('list.focusBackground'), + listFocusForeground: this.colorRegistry.toCssVariableName('list.focusForeground'), + listActiveSelectionBackground: this.colorRegistry.toCssVariableName('list.activeSelectionBackground'), + listActiveSelectionForeground: this.colorRegistry.toCssVariableName('list.ActiveSelectionForeground'), + listActiveSelectionIconForeground: this.colorRegistry.toCssVariableName('list.ActiveSelectionIconForeground'), + listFocusAndSelectionOutline: this.colorRegistry.toCssVariableName('list.FocusAndSelectionOutline'), + listFocusAndSelectionBackground: this.colorRegistry.toCssVariableName('list.ActiveSelectionBackground'), + listFocusAndSelectionForeground: this.colorRegistry.toCssVariableName('list.ActiveSelectionForeground'), + listInactiveSelectionBackground: this.colorRegistry.toCssVariableName('list.InactiveSelectionBackground'), + listInactiveSelectionForeground: this.colorRegistry.toCssVariableName('list.InactiveSelectionForeground'), + listHoverBackground: this.colorRegistry.toCssVariableName('list.HoverBackground'), + listHoverForeground: this.colorRegistry.toCssVariableName('list.HoverForeground'), + listDropBackground: this.colorRegistry.toCssVariableName('list.DropBackground'), + listSelectionOutline: this.colorRegistry.toCssVariableName('activeContrastBorder'), + listHoverOutline: this.colorRegistry.toCssVariableName('activeContrastBorder'), + treeIndentGuidesStroke: this.colorRegistry.toCssVariableName('tree.indentGuidesStroke'), + treeInactiveIndentGuidesStroke: this.colorRegistry.toCssVariableName('tree.inactiveIndentGuidesStroke'), + tableColumnsBorder: this.colorRegistry.toCssVariableName('tree.tableColumnsBorder'), + tableOddRowsBackgroundColor: this.colorRegistry.toCssVariableName('tree.tableOddRowsBackground'), }, inputBox: { - inputForeground: this.colorRegistry.getColor('inputForeground'), - inputBackground: this.colorRegistry.getColor('inputBackground'), - inputBorder: this.colorRegistry.getColor('inputBorder'), - inputValidationInfoBackground: this.colorRegistry.getColor('inputValidation.infoBackground'), - inputValidationInfoForeground: this.colorRegistry.getColor('inputValidation.infoForeground'), - inputValidationInfoBorder: this.colorRegistry.getColor('inputValidation.infoBorder'), - inputValidationWarningBackground: this.colorRegistry.getColor('inputValidation.warningBackground'), - inputValidationWarningForeground: this.colorRegistry.getColor('inputValidation.warningForeground'), - inputValidationWarningBorder: this.colorRegistry.getColor('inputValidation.warningBorder'), - inputValidationErrorBackground: this.colorRegistry.getColor('inputValidation.errorBackground'), - inputValidationErrorForeground: this.colorRegistry.getColor('inputValidation.errorForeground'), - inputValidationErrorBorder: this.colorRegistry.getColor('inputValidation.errorBorder'), + inputForeground: this.colorRegistry.toCssVariableName('inputForeground'), + inputBackground: this.colorRegistry.toCssVariableName('inputBackground'), + inputBorder: this.colorRegistry.toCssVariableName('inputBorder'), + inputValidationInfoBackground: this.colorRegistry.toCssVariableName('inputValidation.infoBackground'), + inputValidationInfoForeground: this.colorRegistry.toCssVariableName('inputValidation.infoForeground'), + inputValidationInfoBorder: this.colorRegistry.toCssVariableName('inputValidation.infoBorder'), + inputValidationWarningBackground: this.colorRegistry.toCssVariableName('inputValidation.warningBackground'), + inputValidationWarningForeground: this.colorRegistry.toCssVariableName('inputValidation.warningForeground'), + inputValidationWarningBorder: this.colorRegistry.toCssVariableName('inputValidation.warningBorder'), + inputValidationErrorBackground: this.colorRegistry.toCssVariableName('inputValidation.errorBackground'), + inputValidationErrorForeground: this.colorRegistry.toCssVariableName('inputValidation.errorForeground'), + inputValidationErrorBorder: this.colorRegistry.toCssVariableName('inputValidation.errorBorder'), }, countBadge: { - badgeBackground: this.colorRegistry.getColor('badge.background'), - badgeForeground: this.colorRegistry.getColor('badge.foreground'), - badgeBorder: this.colorRegistry.getColor('contrastBorder') + badgeBackground: this.colorRegistry.toCssVariableName('badge.background'), + badgeForeground: this.colorRegistry.toCssVariableName('badge.foreground'), + badgeBorder: this.colorRegistry.toCssVariableName('contrastBorder') }, button: { - buttonForeground: this.colorRegistry.getColor('button.foreground'), - buttonBackground: this.colorRegistry.getColor('button.background'), - buttonHoverBackground: this.colorRegistry.getColor('button.hoverBackground'), - buttonBorder: this.colorRegistry.getColor('contrastBorder') + buttonForeground: this.colorRegistry.toCssVariableName('button.foreground'), + buttonBackground: this.colorRegistry.toCssVariableName('button.background'), + buttonHoverBackground: this.colorRegistry.toCssVariableName('button.hoverBackground'), + buttonBorder: this.colorRegistry.toCssVariableName('contrastBorder'), + buttonSeparator: this.colorRegistry.toCssVariableName('button.Separator'), + buttonSecondaryForeground: this.colorRegistry.toCssVariableName('button.secondaryForeground'), + buttonSecondaryBackground: this.colorRegistry.toCssVariableName('button.secondaryBackground'), + buttonSecondaryHoverBackground: this.colorRegistry.toCssVariableName('button.secondaryHoverBackground'), }, progressBar: { - progressBarBackground: this.colorRegistry.getColor('progressBar.background') + progressBarBackground: this.colorRegistry.toCssVariableName('progressBar.background') }, keybindingLabel: { - keybindingLabelBackground: this.colorRegistry.getColor('keybindingLabe.background'), - keybindingLabelForeground: this.colorRegistry.getColor('keybindingLabel.foreground'), - keybindingLabelBorder: this.colorRegistry.getColor('keybindingLabel.border'), - keybindingLabelBottomBorder: this.colorRegistry.getColor('keybindingLabel.bottomBorder'), - keybindingLabelShadow: this.colorRegistry.getColor('widget.shadow') + keybindingLabelBackground: this.colorRegistry.toCssVariableName('keybindingLabel.background'), + keybindingLabelForeground: this.colorRegistry.toCssVariableName('keybindingLabel.foreground'), + keybindingLabelBorder: this.colorRegistry.toCssVariableName('keybindingLabel.border'), + keybindingLabelBottomBorder: this.colorRegistry.toCssVariableName('keybindingLabel.bottomBorder'), + keybindingLabelShadow: this.colorRegistry.toCssVariableName('widget.shadow') }, }; } @@ -282,7 +318,8 @@ export class MonacoQuickInputService implements QuickInputService { protected readonly keybindingRegistry: KeybindingRegistry; get backButton(): QuickInputButton { - return this.monacoService.backButton; + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + return this.monacoService.backButton as QuickInputButton; } get onShow(): Event { return this.monacoService.onShow; } @@ -293,7 +330,8 @@ export class MonacoQuickInputService implements QuickInputService { } createInputBox(): InputBox { - return this.monacoService.createInputBox(); + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + return this.monacoService.createInputBox() as InputBox; } input(options?: InputOptions, token?: monaco.CancellationToken): Promise { @@ -309,39 +347,9 @@ export class MonacoQuickInputService implements QuickInputService { } async pick = PickOptions>( - picks: Promise[]> | QuickPickInput[], options?: O, token?: monaco.CancellationToken - ): Promise<(O extends { canPickMany: true; } ? T[] : T) | undefined> { - type M = T & { buttons?: NormalizedQuickInputButton[] }; - type R = (O extends { canPickMany: true; } ? T[] : T); - - const monacoPicks: Promise[]> = new Promise(async resolve => { - const updatedPicks = (await picks).map(pick => { - if (pick.type !== 'separator') { - const icon = pick.iconPath; - // @monaco-uplift - // Other kind of icons (URI and URI dark/light) shall be supported once monaco editor has been upgraded to at least 1.81. - // see https://github.com/eclipse-theia/theia/pull/12945#issue-1913645228 - if (ThemeIcon.isThemeIcon(icon)) { - const codicon = codiconArray(icon.id); - if (pick.iconClasses) { - pick.iconClasses.push(...codicon); - } else { - pick.iconClasses = codicon; - } - } - pick.buttons &&= pick.buttons.map(QuickInputButton.normalize); - } - return pick as M; - }); - resolve(updatedPicks); - }); - const monacoOptions = options as IPickOptions; - const picked = await this.monacoService.pick(monacoPicks, monacoOptions, token); - if (!picked) { return picked; } - if (options?.canPickMany) { - return (Array.isArray(picked) ? picked : [picked]) as R; - } - return Array.isArray(picked) ? picked[0] : picked; + picks: Promise[]> | QuickPickInput[], options?: O, token?: CancellationToken + ): Promise { + return this.monacoService.pick(picks, options, token); } showQuickPick(items: Array, options?: QuickPickOptions): Promise { @@ -367,21 +375,6 @@ export class MonacoQuickInputService implements QuickInputService { wrapped.activeItems = [options.activeItem]; } - wrapped.onDidAccept(() => { - if (options?.onDidAccept) { - options.onDidAccept(); - } - wrapped.hide(); - resolve(wrapped.selectedItems[0]); - }); - - wrapped.onDidHide(() => { - if (options.onDidHide) { - options.onDidHide(); - }; - wrapped.dispose(); - setTimeout(() => resolve(undefined)); - }); wrapped.onDidChangeValue((filter: string) => { if (options.onDidChangeValue) { options.onDidChangeValue(wrapped, filter); @@ -394,7 +387,8 @@ export class MonacoQuickInputService implements QuickInputService { }); wrapped.onDidTriggerButton((button: IQuickInputButton) => { if (options.onDidTriggerButton) { - options.onDidTriggerButton(button); + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + options.onDidTriggerButton(button as QuickInputButton); } }); wrapped.onDidTriggerItemButton((event: QuickPickItemButtonEvent) => { @@ -416,6 +410,20 @@ export class MonacoQuickInputService implements QuickInputService { } }); } + wrapped.onDidAccept(() => { + if (options?.onDidAccept) { + options.onDidAccept(); + } + wrapped.hide(); + resolve(wrapped.selectedItems[0]); + }); + wrapped.onDidHide(() => { + if (options?.onDidHide) { + options?.onDidHide(); + }; + wrapped.dispose(); + setTimeout(() => resolve(undefined)); + }); wrapped.show(); }).then(item => { if (item?.execute) { @@ -577,7 +585,22 @@ class MonacoQuickPick extends MonacoQuickInput implemen } get items(): readonly (T | QuickPickSeparator)[] { - return this.wrapped.items.map(item => QuickPickSeparator.is(item) ? item : item.item); + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + return this.wrapped.items.map(item => { + if (item instanceof MonacoQuickPickItem) { + return item.item; + } else { + return item; + } + }); + } + + get buttons(): ReadonlyArray { + return this.wrapped.buttons as QuickInputButton[]; + } + + set buttons(buttons: ReadonlyArray) { + this.wrapped.buttons = buttons; } set items(itemList: readonly (T | QuickPickSeparator)[]) { @@ -610,12 +633,14 @@ class MonacoQuickPick extends MonacoQuickInput implemen readonly onDidAccept: Event<{ inBackground: boolean }> = this.wrapped.onDidAccept; readonly onDidChangeValue: Event = this.wrapped.onDidChangeValue; - readonly onDidTriggerButton: Event = this.wrapped.onDidTriggerButton; + + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + readonly onDidTriggerButton: Event = this.wrapped.onDidTriggerButton as Event; readonly onDidTriggerItemButton: Event> = Event.map(this.wrapped.onDidTriggerItemButton, (evt: IQuickPickItemButtonEvent>) => ({ item: evt.item.item, button: evt.button - })); + })) as Event>; readonly onDidChangeActive: Event = Event.map( this.wrapped.onDidChangeActive, (items: MonacoQuickPickItem[]) => items.map(item => item.item)); @@ -630,7 +655,7 @@ class MonacoQuickPick extends MonacoQuickInput implemen const monacoReferences: MonacoQuickPickItem[] = []; for (const item of items) { for (const wrappedItem of source) { - if (!QuickPickSeparator.is(wrappedItem) && wrappedItem.item === item) { + if (wrappedItem instanceof MonacoQuickPickItem && wrappedItem.item === item) { monacoReferences.push(wrappedItem); } } @@ -663,7 +688,7 @@ export class MonacoQuickPickItem implements IQuickPickI this.detail = item.detail; this.keybinding = item.keySequence ? new MonacoResolvedKeybinding(item.keySequence, kbRegistry) : undefined; this.iconClasses = item.iconClasses; - this.buttons = item.buttons?.map(QuickInputButton.normalize); + this.buttons = item.buttons; this.alwaysShow = item.alwaysShow; this.highlights = item.highlights; } diff --git a/packages/monaco/src/browser/monaco-resolved-keybinding.ts b/packages/monaco/src/browser/monaco-resolved-keybinding.ts index 526af0b90c390..347b85d96b373 100644 --- a/packages/monaco/src/browser/monaco-resolved-keybinding.ts +++ b/packages/monaco/src/browser/monaco-resolved-keybinding.ts @@ -16,7 +16,7 @@ import { KeyCode as MonacoKeyCode } from '@theia/monaco-editor-core/esm/vs/base/common/keyCodes'; import { - ChordKeybinding, KeybindingModifier, ResolvedKeybinding, ResolvedKeybindingPart, ScanCodeBinding, SimpleKeybinding + ResolvedKeybinding, ResolvedChord, SingleModifierChord, KeyCodeChord, Chord } from '@theia/monaco-editor-core/esm/vs/base/common/keybindings'; import { ElectronAcceleratorLabelProvider, UILabelProvider, UserSettingsLabelProvider } from '@theia/monaco-editor-core/esm/vs/base/common/keybindingLabels'; import { USLayoutResolvedKeybinding } from '@theia/monaco-editor-core/esm/vs/platform/keybinding/common/usLayoutResolvedKeybinding'; @@ -28,15 +28,15 @@ import { KEY_CODE_MAP } from './monaco-keycode-map'; export class MonacoResolvedKeybinding extends ResolvedKeybinding { - protected readonly parts: ResolvedKeybindingPart[]; + protected readonly chords: ResolvedChord[]; constructor(protected readonly keySequence: KeySequence, keybindingService: KeybindingRegistry) { super(); - this.parts = keySequence.map(keyCode => { + this.chords = keySequence.map(keyCode => { // eslint-disable-next-line no-null/no-null const keyLabel = keyCode.key ? keybindingService.acceleratorForKey(keyCode.key) : null; const keyAriaLabel = keyLabel; - return new ResolvedKeybindingPart( + return new ResolvedChord( keyCode.ctrl, keyCode.shift, keyCode.alt, @@ -48,43 +48,43 @@ export class MonacoResolvedKeybinding extends ResolvedKeybinding { } getLabel(): string | null { - return UILabelProvider.toLabel(MonacoPlatform.OS, this.parts, p => p.keyLabel); + return UILabelProvider.toLabel(MonacoPlatform.OS, this.chords, p => p.keyLabel); } getAriaLabel(): string | null { - return UILabelProvider.toLabel(MonacoPlatform.OS, this.parts, p => p.keyAriaLabel); + return UILabelProvider.toLabel(MonacoPlatform.OS, this.chords, p => p.keyAriaLabel); } getElectronAccelerator(): string | null { - if (this.isChord()) { + if (this.hasMultipleChords()) { // Electron cannot handle chords // eslint-disable-next-line no-null/no-null return null; } - return ElectronAcceleratorLabelProvider.toLabel(MonacoPlatform.OS, this.parts, p => p.keyLabel); + return ElectronAcceleratorLabelProvider.toLabel(MonacoPlatform.OS, this.chords, p => p.keyLabel); } getUserSettingsLabel(): string | null { - return UserSettingsLabelProvider.toLabel(MonacoPlatform.OS, this.parts, p => p.keyLabel); + return UserSettingsLabelProvider.toLabel(MonacoPlatform.OS, this.chords, p => p.keyLabel); } isWYSIWYG(): boolean { return true; } - isChord(): boolean { - return this.parts.length > 1; + hasMultipleChords(): boolean { + return this.chords.length > 1; } - getDispatchParts(): (string | null)[] { + getDispatchChords(): (string | null)[] { return this.keySequence.map(keyCode => USLayoutResolvedKeybinding.getDispatchStr(this.toKeybinding(keyCode))); } - getSingleModifierDispatchParts(): (KeybindingModifier | null)[] { + getSingleModifierDispatchChords(): (SingleModifierChord | null)[] { return this.keySequence.map(keybinding => this.getSingleModifierDispatchPart(keybinding)); } - protected getSingleModifierDispatchPart(code: KeyCode): KeybindingModifier | null { + protected getSingleModifierDispatchPart(code: KeyCode): SingleModifierChord | null { if (code.key?.keyCode === undefined) { return null; // eslint-disable-line no-null/no-null } @@ -103,8 +103,8 @@ export class MonacoResolvedKeybinding extends ResolvedKeybinding { return null; // eslint-disable-line no-null/no-null } - private toKeybinding(keyCode: KeyCode): SimpleKeybinding { - return new SimpleKeybinding( + private toKeybinding(keyCode: KeyCode): KeyCodeChord { + return new KeyCodeChord( keyCode.ctrl, keyCode.shift, keyCode.alt, @@ -113,16 +113,16 @@ export class MonacoResolvedKeybinding extends ResolvedKeybinding { ); } - public getParts(): ResolvedKeybindingPart[] { - return this.parts; + public getChords(): ResolvedChord[] { + return this.chords; } - static toKeybinding(keybindings: Array): string { + static toKeybinding(keybindings: Array): string { return keybindings.map(binding => this.keyCode(binding)).join(' '); } - static keyCode(keybinding: SimpleKeybinding | ScanCodeBinding): KeyCode { - const keyCode = keybinding instanceof SimpleKeybinding ? keybinding.keyCode : USLayoutResolvedKeybinding['_scanCodeToKeyCode'](keybinding.scanCode); + static keyCode(keybinding: Chord): KeyCode { + const keyCode = keybinding instanceof KeyCodeChord ? keybinding.keyCode : USLayoutResolvedKeybinding['_scanCodeToKeyCode'](keybinding.scanCode); const sequence: Keystroke = { first: Key.getKey(this.monaco2BrowserKeyCode(keyCode & 0xff)), modifiers: [] @@ -146,8 +146,8 @@ export class MonacoResolvedKeybinding extends ResolvedKeybinding { return KeyCode.createKeyCode(sequence); } - static keySequence(keybinding: ChordKeybinding): KeySequence { - return keybinding.parts.map(part => this.keyCode(part)); + static keySequence(keybinding: Chord[]): KeySequence { + return keybinding.map(part => this.keyCode(part)); } private static monaco2BrowserKeyCode(keyCode: MonacoKeyCode): number { diff --git a/packages/monaco/src/browser/monaco-standalone-theme-service.ts b/packages/monaco/src/browser/monaco-standalone-theme-service.ts new file mode 100644 index 0000000000000..cae2bffd3f3c7 --- /dev/null +++ b/packages/monaco/src/browser/monaco-standalone-theme-service.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// ***************************************************************************** +// 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 { IDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle'; +import { StandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneThemeService'; + +export class MonacoStandaloneThemeService extends StandaloneThemeService { + protected get styleElements(): HTMLStyleElement[] { + // access private style element array + return (this as any)._styleElements; + } + + protected get allCSS(): string { + return (this as any)._allCSS; + } + + override registerEditorContainer(domNode: HTMLElement): IDisposable { + const style = domNode.ownerDocument.createElement('style'); + style.type = 'text/css'; + style.media = 'screen'; + style.className = 'monaco-colors'; + style.textContent = this.allCSS; + domNode.ownerDocument.head.appendChild(style); + this.styleElements.push(style); + return { + dispose: () => { + for (let i = 0; i < this.styleElements.length; i++) { + if (this.styleElements[i] === style) { + this.styleElements.splice(i, 1); + style.remove(); + return; + } + } + } + }; + } +} diff --git a/packages/monaco/src/browser/monaco-status-bar-contribution.ts b/packages/monaco/src/browser/monaco-status-bar-contribution.ts index fae85805e9f1a..c7ac733d29987 100644 --- a/packages/monaco/src/browser/monaco-status-bar-contribution.ts +++ b/packages/monaco/src/browser/monaco-status-bar-contribution.ts @@ -14,93 +14,90 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject } from '@theia/core/shared/inversify'; +import { injectable } from '@theia/core/shared/inversify'; import { DisposableCollection, nls } from '@theia/core'; -import { FrontendApplicationContribution, FrontendApplication, StatusBar, StatusBarAlignment } from '@theia/core/lib/browser'; -import { EditorCommands, EditorManager, EditorWidget } from '@theia/editor/lib/browser'; +import { StatusBar, StatusBarAlignment, Widget, WidgetStatusBarContribution } from '@theia/core/lib/browser'; +import { EditorCommands, EditorWidget } from '@theia/editor/lib/browser'; import { MonacoEditor } from './monaco-editor'; import * as monaco from '@theia/monaco-editor-core'; +export const EDITOR_STATUS_TABBING_CONFIG = 'editor-status-tabbing-config'; +export const EDITOR_STATUS_EOL = 'editor-status-eol'; + @injectable() -export class MonacoStatusBarContribution implements FrontendApplicationContribution { +export class MonacoStatusBarContribution implements WidgetStatusBarContribution { protected readonly toDispose = new DisposableCollection(); - constructor( - @inject(EditorManager) protected readonly editorManager: EditorManager, - @inject(StatusBar) protected readonly statusBar: StatusBar - ) { } - - onStart(app: FrontendApplication): void { - this.updateStatusBar(); - this.editorManager.onCurrentEditorChanged(() => this.updateStatusBar()); + canHandle(widget: Widget): widget is EditorWidget { + if (widget instanceof EditorWidget) { + return Boolean(this.getModel(widget)); + } + return false; } - protected updateStatusBar(): void { - const editor = this.editorManager.currentEditor; + activate(statusBar: StatusBar, editor: EditorWidget): void { + this.toDispose.dispose(); const editorModel = this.getModel(editor); - if (editor && editorModel) { - this.setConfigTabSizeWidget(); - this.setLineEndingWidget(); - - this.toDispose.dispose(); + if (editorModel) { + this.setConfigTabSizeWidget(statusBar, editorModel); + this.setLineEndingWidget(statusBar, editorModel); this.toDispose.push(editorModel.onDidChangeOptions(() => { - this.setConfigTabSizeWidget(); - this.setLineEndingWidget(); + this.setConfigTabSizeWidget(statusBar, editorModel); + this.setLineEndingWidget(statusBar, editorModel); })); let previous = editorModel.getEOL(); this.toDispose.push(editorModel.onDidChangeContent(e => { if (previous !== e.eol) { previous = e.eol; - this.setLineEndingWidget(); + this.setLineEndingWidget(statusBar, editorModel); } })); } else { - this.removeConfigTabSizeWidget(); - this.removeLineEndingWidget(); + this.deactivate(statusBar); } } - protected setConfigTabSizeWidget(): void { - const editor = this.editorManager.currentEditor; - const editorModel = this.getModel(editor); - if (editor && editorModel) { - const modelOptions = editorModel.getOptions(); - const tabSize = modelOptions.tabSize; - const indentSize = modelOptions.indentSize; - const spaceOrTabSizeMessage = modelOptions.insertSpaces - ? nls.localizeByDefault('Spaces: {0}', indentSize) - : nls.localizeByDefault('Tab Size: {0}', tabSize); - this.statusBar.setElement('editor-status-tabbing-config', { - text: spaceOrTabSizeMessage, - alignment: StatusBarAlignment.RIGHT, - priority: 10, - command: EditorCommands.CONFIG_INDENTATION.id, - tooltip: nls.localizeByDefault('Select Indentation') - }); - } + deactivate(statusBar: StatusBar): void { + this.toDispose.dispose(); + this.removeConfigTabSizeWidget(statusBar); + this.removeLineEndingWidget(statusBar); } - protected removeConfigTabSizeWidget(): void { - this.statusBar.removeElement('editor-status-tabbing-config'); + + protected setConfigTabSizeWidget(statusBar: StatusBar, model: monaco.editor.ITextModel): void { + const modelOptions = model.getOptions(); + const tabSize = modelOptions.tabSize; + const indentSize = modelOptions.indentSize; + const spaceOrTabSizeMessage = modelOptions.insertSpaces + ? nls.localizeByDefault('Spaces: {0}', indentSize) + : nls.localizeByDefault('Tab Size: {0}', tabSize); + statusBar.setElement(EDITOR_STATUS_TABBING_CONFIG, { + text: spaceOrTabSizeMessage, + alignment: StatusBarAlignment.RIGHT, + priority: 10, + command: EditorCommands.CONFIG_INDENTATION.id, + tooltip: nls.localizeByDefault('Select Indentation') + }); } - protected setLineEndingWidget(): void { - const editor = this.editorManager.currentEditor; - const editorModel = this.getModel(editor); - if (editor && editorModel) { - const eol = editorModel.getEOL(); - const text = eol === '\n' ? 'LF' : 'CRLF'; - this.statusBar.setElement('editor-status-eol', { - text: `${text}`, - alignment: StatusBarAlignment.RIGHT, - priority: 11, - command: EditorCommands.CONFIG_EOL.id, - tooltip: nls.localizeByDefault('Select End of Line Sequence') - }); - } + protected removeConfigTabSizeWidget(statusBar: StatusBar): void { + statusBar.removeElement(EDITOR_STATUS_TABBING_CONFIG); } - protected removeLineEndingWidget(): void { - this.statusBar.removeElement('editor-status-eol'); + + protected setLineEndingWidget(statusBar: StatusBar, model: monaco.editor.ITextModel): void { + const eol = model.getEOL(); + const text = eol === '\n' ? 'LF' : 'CRLF'; + statusBar.setElement(EDITOR_STATUS_EOL, { + text: `${text}`, + alignment: StatusBarAlignment.RIGHT, + priority: 11, + command: EditorCommands.CONFIG_EOL.id, + tooltip: nls.localizeByDefault('Select End of Line Sequence') + }); + } + + protected removeLineEndingWidget(statusBar: StatusBar): void { + statusBar.removeElement(EDITOR_STATUS_EOL); } protected getModel(editor: EditorWidget | undefined): monaco.editor.ITextModel | undefined { diff --git a/packages/monaco/src/browser/monaco-text-model-service.ts b/packages/monaco/src/browser/monaco-text-model-service.ts index d4440b4d80946..6ba76b63c512c 100644 --- a/packages/monaco/src/browser/monaco-text-model-service.ts +++ b/packages/monaco/src/browser/monaco-text-model-service.ts @@ -45,13 +45,6 @@ export interface MonacoEditorModelFactory { export class MonacoTextModelService implements ITextModelService { declare readonly _serviceBrand: undefined; - /** - * This component does some asynchronous work before being fully initialized. - * - * @deprecated since 1.25.0. Is instantly resolved. - */ - readonly ready: Promise = Promise.resolve(); - protected readonly _models = new ReferenceCollection( uri => this.loadModel(new URI(uri)) ); @@ -109,14 +102,18 @@ export class MonacoTextModelService implements ITextModelService { return this._models.acquire(raw.toString()); } - protected async loadModel(uri: URI): Promise { + /** + * creates a model which is not saved by the model service. + * this will therefore also not be created on backend side. + */ + createUnmanagedModel(raw: monaco.Uri | URI): Promise { + return this.loadModel(new URI(raw.toString())); + } + + async loadModel(uri: URI): Promise { await this.editorPreferences.ready; const resource = await this.resourceProvider(uri); const model = await (await this.createModel(resource)).load(); - this.updateModel(model); - model.textEditorModel.onDidChangeLanguage(() => this.updateModel(model)); - const disposable = this.editorPreferences.onPreferenceChanged(change => this.updateModel(model, change)); - model.onDispose(() => disposable.dispose()); return model; } @@ -146,45 +143,6 @@ export class MonacoTextModelService implements ITextModelService { return undefined; } - protected updateModel(model: MonacoEditorModel, change?: EditorPreferenceChange): void { - if (!change) { - model.autoSave = this.editorPreferences.get('files.autoSave', undefined, model.uri); - model.autoSaveDelay = this.editorPreferences.get('files.autoSaveDelay', undefined, model.uri); - model.textEditorModel.updateOptions(this.getModelOptions(model)); - } else if (change.affects(model.uri, model.languageId)) { - if (change.preferenceName === 'files.autoSave') { - model.autoSave = this.editorPreferences.get('files.autoSave', undefined, model.uri); - } - if (change.preferenceName === 'files.autoSaveDelay') { - model.autoSaveDelay = this.editorPreferences.get('files.autoSaveDelay', undefined, model.uri); - } - const modelOption = this.toModelOption(change.preferenceName); - if (modelOption) { - model.textEditorModel.updateOptions(this.getModelOptions(model)); - } - } - } - - /** @deprecated pass MonacoEditorModel instead */ - protected getModelOptions(uri: string): ITextModelUpdateOptions; - protected getModelOptions(model: MonacoEditorModel): ITextModelUpdateOptions; - protected getModelOptions(arg: string | MonacoEditorModel): ITextModelUpdateOptions { - const uri = typeof arg === 'string' ? arg : arg.uri; - const overrideIdentifier = typeof arg === 'string' ? undefined : arg.languageId; - return { - tabSize: this.editorPreferences.get({ preferenceName: 'editor.tabSize', overrideIdentifier }, undefined, uri), - // @monaco-uplift: when available, switch to 'editor.indentSize' preference. - indentSize: this.editorPreferences.get({ preferenceName: 'editor.tabSize', overrideIdentifier }, undefined, uri), - insertSpaces: this.editorPreferences.get({ preferenceName: 'editor.insertSpaces', overrideIdentifier }, undefined, uri), - bracketColorizationOptions: { - enabled: this.editorPreferences.get({ preferenceName: 'editor.bracketPairColorization.enabled', overrideIdentifier }, undefined, uri), - independentColorPoolPerBracketType: this.editorPreferences.get( - { preferenceName: 'editor.bracketPairColorization.independentColorPoolPerBracketType', overrideIdentifier }, undefined, uri), - }, - trimAutoWhitespace: this.editorPreferences.get({ preferenceName: 'editor.trimAutoWhitespace', overrideIdentifier }, undefined, uri), - }; - } - registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { return { dispose(): void { @@ -194,6 +152,6 @@ export class MonacoTextModelService implements ITextModelService { } canHandleResource(resource: monaco.Uri): boolean { - return this.fileService.canHandleResource(new URI(resource)); + return this.fileService.canHandleResource(URI.fromComponents(resource)); } } diff --git a/packages/monaco/src/browser/monaco-to-protocol-converter.ts b/packages/monaco/src/browser/monaco-to-protocol-converter.ts index 4b054e8fb8ab5..c88629e92b5a5 100644 --- a/packages/monaco/src/browser/monaco-to-protocol-converter.ts +++ b/packages/monaco/src/browser/monaco-to-protocol-converter.ts @@ -18,6 +18,7 @@ import { injectable } from '@theia/core/shared/inversify'; import { Position, Range } from '@theia/core/shared/vscode-languageserver-protocol'; import { RecursivePartial } from '@theia/core/lib/common/types'; import * as monaco from '@theia/monaco-editor-core'; +import { Selection } from '@theia/editor/lib/browser'; export interface MonacoRangeReplace { insert: monaco.IRange; @@ -68,4 +69,14 @@ export class MonacoToProtocolConverter { } } + asSelection(selection: monaco.Selection): Selection { + const start = this.asPosition(selection.selectionStartLineNumber, selection.selectionStartColumn); + const end = this.asPosition(selection.positionLineNumber, selection.positionColumn); + return { + start, + end, + direction: selection.getDirection() === monaco.SelectionDirection.LTR ? 'ltr' : 'rtl' + }; + } + } diff --git a/packages/monaco/src/browser/monaco-undo-redo-handler.ts b/packages/monaco/src/browser/monaco-undo-redo-handler.ts new file mode 100644 index 0000000000000..51a5e51c1069d --- /dev/null +++ b/packages/monaco/src/browser/monaco-undo-redo-handler.ts @@ -0,0 +1,64 @@ +// ***************************************************************************** +// 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 { UndoRedoHandler } from '@theia/core/lib/browser'; +import { injectable } from '@theia/core/shared/inversify'; +import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; + +@injectable() +export abstract class AbstractMonacoUndoRedoHandler implements UndoRedoHandler { + priority: number; + abstract select(): ICodeEditor | undefined; + undo(item: ICodeEditor): void { + item.trigger('MonacoUndoRedoHandler', 'undo', undefined); + } + redo(item: ICodeEditor): void { + item.trigger('MonacoUndoRedoHandler', 'redo', undefined); + } +} + +@injectable() +export class FocusedMonacoUndoRedoHandler extends AbstractMonacoUndoRedoHandler { + override priority = 10000; + + protected codeEditorService = StandaloneServices.get(ICodeEditorService); + + override select(): ICodeEditor | undefined { + const focusedEditor = this.codeEditorService.getFocusedCodeEditor(); + if (focusedEditor && focusedEditor.hasTextFocus()) { + return focusedEditor; + } + return undefined; + } +} + +@injectable() +export class ActiveMonacoUndoRedoHandler extends AbstractMonacoUndoRedoHandler { + override priority = 0; + + protected codeEditorService = StandaloneServices.get(ICodeEditorService); + + override select(): ICodeEditor | undefined { + const focusedEditor = this.codeEditorService.getActiveCodeEditor(); + if (focusedEditor) { + focusedEditor.focus(); + return focusedEditor; + } + return undefined; + } +} diff --git a/packages/monaco/src/browser/monaco-workspace.ts b/packages/monaco/src/browser/monaco-workspace.ts index b832a74b89a0a..6debea8d9d55d 100644 --- a/packages/monaco/src/browser/monaco-workspace.ts +++ b/packages/monaco/src/browser/monaco-workspace.ts @@ -42,6 +42,7 @@ import { SnippetParser } from '@theia/monaco-editor-core/esm/vs/editor/contrib/s import { TextEdit } from '@theia/monaco-editor-core/esm/vs/editor/common/languages'; import { SnippetController2 } from '@theia/monaco-editor-core/esm/vs/editor/contrib/snippet/browser/snippetController2'; import { isObject, MaybePromise, nls } from '@theia/core/lib/common'; +import { SaveableService } from '@theia/core/lib/browser'; export namespace WorkspaceFileEdit { export function is(arg: Edit): arg is monaco.languages.IWorkspaceFileEdit { @@ -124,6 +125,9 @@ export class MonacoWorkspace { @inject(ProblemManager) protected readonly problems: ProblemManager; + @inject(SaveableService) + protected readonly saveService: SaveableService; + @postConstruct() protected init(): void { this.resolveReady(); @@ -192,7 +196,7 @@ export class MonacoWorkspace { // acquired by the editor, thus losing the changes that made it dirty. this.textModelService.createModelReference(model.textEditorModel.uri).then(ref => { ( - model.autoSave !== 'off' ? new Promise(resolve => model.onDidSaveModel(resolve)) : + this.saveService.autoSave !== 'off' ? new Promise(resolve => model.onDidSaveModel(resolve)) : this.editorManager.open(new URI(model.uri), { mode: 'open' }) ).then( () => ref.dispose() @@ -230,7 +234,7 @@ export class MonacoWorkspace { }); } - async applyBulkEdit(edits: ResourceEdit[], options?: IBulkEditOptions): Promise { + async applyBulkEdit(edits: ResourceEdit[], options?: IBulkEditOptions): Promise { try { let totalEdits = 0; let totalFiles = 0; @@ -264,12 +268,12 @@ export class MonacoWorkspace { } const ariaSummary = this.getAriaSummary(totalEdits, totalFiles); - return { ariaSummary, success: true }; + return { ariaSummary, isApplied: true }; } catch (e) { console.error('Failed to apply Resource edits:', e); return { ariaSummary: `Error applying Resource edits: ${e.toString()}`, - success: false + isApplied: false }; } } @@ -369,27 +373,27 @@ export class MonacoWorkspace { const options = edit.options || {}; if (edit.newResource && edit.oldResource) { // rename - if (options.overwrite === undefined && options.ignoreIfExists && await this.fileService.exists(new URI(edit.newResource))) { + if (options.overwrite === undefined && options.ignoreIfExists && await this.fileService.exists(URI.fromComponents(edit.newResource))) { return; // not overwriting, but ignoring, and the target file exists } - await this.fileService.move(new URI(edit.oldResource), new URI(edit.newResource), { overwrite: options.overwrite }); + await this.fileService.move(URI.fromComponents(edit.oldResource), URI.fromComponents(edit.newResource), { overwrite: options.overwrite }); } else if (!edit.newResource && edit.oldResource) { // delete file - if (await this.fileService.exists(new URI(edit.oldResource))) { + if (await this.fileService.exists(URI.fromComponents(edit.oldResource))) { let useTrash = this.filePreferences['files.enableTrash']; - if (useTrash && !(this.fileService.hasCapability(new URI(edit.oldResource), FileSystemProviderCapabilities.Trash))) { + if (useTrash && !(this.fileService.hasCapability(URI.fromComponents(edit.oldResource), FileSystemProviderCapabilities.Trash))) { useTrash = false; // not supported by provider } - await this.fileService.delete(new URI(edit.oldResource), { useTrash, recursive: options.recursive }); + await this.fileService.delete(URI.fromComponents(edit.oldResource), { useTrash, recursive: options.recursive }); } else if (!options.ignoreIfNotExists) { throw new Error(`${edit.oldResource} does not exist and can not be deleted`); } } else if (edit.newResource && !edit.oldResource) { // create file - if (options.overwrite === undefined && options.ignoreIfExists && await this.fileService.exists(new URI(edit.newResource))) { + if (options.overwrite === undefined && options.ignoreIfExists && await this.fileService.exists(URI.fromComponents(edit.newResource))) { return; // not overwriting, but ignoring, and the target file exists } - await this.fileService.create(new URI(edit.newResource), undefined, { overwrite: options.overwrite }); + await this.fileService.create(URI.fromComponents(edit.newResource), undefined, { overwrite: options.overwrite }); } } } diff --git a/packages/monaco/src/browser/simple-monaco-editor.ts b/packages/monaco/src/browser/simple-monaco-editor.ts index 61096e5437796..5c862b81fba94 100644 --- a/packages/monaco/src/browser/simple-monaco-editor.ts +++ b/packages/monaco/src/browser/simple-monaco-editor.ts @@ -16,7 +16,7 @@ import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor'; -import { CodeEditorWidget } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/codeEditorWidget'; import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation'; import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { ServiceCollection } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/serviceCollection'; @@ -25,6 +25,8 @@ import { MonacoEditorModel } from './monaco-editor-model'; import { Dimension, EditorMouseEvent, MouseTarget, Position, TextDocumentChangeEvent } from '@theia/editor/lib/browser'; import * as monaco from '@theia/monaco-editor-core'; import { ElementExt } from '@theia/core/shared/@phosphor/domutils'; +import { Selection } from '@theia/editor/lib/browser/editor'; +import { SelectionDirection } from '@theia/monaco-editor-core/esm/vs/editor/common/core/selection'; export class SimpleMonacoEditor extends MonacoEditorServices implements Disposable { @@ -32,7 +34,6 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab protected readonly toDispose = new DisposableCollection(); protected readonly onCursorPositionChangedEmitter = new Emitter(); - protected readonly onSelectionChangedEmitter = new Emitter(); protected readonly onFocusChangedEmitter = new Emitter(); protected readonly onDocumentContentChangedEmitter = new Emitter(); readonly onDocumentContentChanged = this.onDocumentContentChangedEmitter.event; @@ -43,6 +44,7 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab readonly onEncodingChanged = this.document.onDidChangeEncoding; protected readonly onResizeEmitter = new Emitter(); readonly onDidResize = this.onResizeEmitter.event; + readonly onDidChangeReadOnly = this.document.onDidChangeReadOnly; constructor( readonly uri: URI, @@ -50,19 +52,22 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab readonly node: HTMLElement, services: MonacoEditorServices, options?: MonacoEditor.IOptions, - override?: EditorServiceOverrides + override?: EditorServiceOverrides, + widgetOptions?: ICodeEditorWidgetOptions ) { super(services); this.toDispose.pushAll([ this.onCursorPositionChangedEmitter, - this.onSelectionChangedEmitter, this.onFocusChangedEmitter, this.onDocumentContentChangedEmitter, this.onMouseDownEmitter, this.onLanguageChangedEmitter, this.onScrollChangedEmitter ]); - this.toDispose.push(this.create(options, override)); + this.toDispose.push(this.create({ + ...MonacoEditor.createReadOnlyOptions(document.readOnly), + ...options + }, override, widgetOptions)); this.addHandlers(this.editor); this.editor.setModel(document.textEditorModel); } @@ -71,7 +76,15 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab return this.editor; } - protected create(options?: MonacoEditor.IOptions, override?: EditorServiceOverrides): Disposable { + onSelectionChanged(listener: (range: Selection) => void): Disposable { + return this.editor.onDidChangeCursorSelection(event => + listener({ + ...this.m2p.asRange(event.selection), + direction: event.selection.getDirection() === SelectionDirection.LTR ? 'ltr' : 'rtl' + })); + } + + protected create(options?: MonacoEditor.IOptions, override?: EditorServiceOverrides, widgetOptions?: ICodeEditorWidgetOptions): Disposable { const combinedOptions = { ...options, lightbulb: { enabled: true }, @@ -93,9 +106,7 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab width: 0, height: 0 }, - }, { - - }); + }, widgetOptions ?? {}); } protected addHandlers(codeEditor: CodeEditorWidget): void { @@ -125,6 +136,9 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab this.toDispose.push(codeEditor.onDidScrollChange(e => { this.onScrollChangedEmitter.fire(undefined); })); + this.toDispose.push(this.onDidChangeReadOnly(readOnly => { + codeEditor.updateOptions(MonacoEditor.createReadOnlyOptions(readOnly)); + })); } setLanguage(languageId: string): void { @@ -136,7 +150,7 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab } protected getInstantiatorWithOverrides(override?: EditorServiceOverrides): IInstantiationService { - const instantiator = StandaloneServices.initialize({}); + const instantiator = StandaloneServices.get(IInstantiationService); if (override) { const overrideServices = new ServiceCollection(...override); return instantiator.createChild(overrideServices); @@ -152,6 +166,10 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab }; } + focus(): void { + this.editor.focus(); + } + refresh(): void { this.autoresize(); } diff --git a/packages/monaco/src/browser/style/index.css b/packages/monaco/src/browser/style/index.css index 577f853ef3d4e..d03b5b4043382 100644 --- a/packages/monaco/src/browser/style/index.css +++ b/packages/monaco/src/browser/style/index.css @@ -21,7 +21,6 @@ .monaco-editor .zone-widget { position: absolute; z-index: 10; - background-color: var(--theia-editorWidget-background); } .monaco-editor .zone-widget .zone-widget-container { @@ -177,6 +176,10 @@ text-align: left; } +.quick-input-list .monaco-icon-label::before { + height: 22px; +} + .codicon-file.default-file-icon.file-icon { padding-left: 2px; height: 22px; diff --git a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts index 0f0eeca3b923a..2f4b8220e62d0 100644 --- a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts +++ b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts @@ -178,7 +178,7 @@ export class MonacoTextmateService implements FrontendApplicationContribution { protected waitForLanguage(language: string, cb: () => {}): Disposable { const languageService = StandaloneServices.get(ILanguageService) as LanguageService; - if (languageService['_encounteredLanguages'].has(language)) { + if (languageService['_requestedBasicLanguages'].has(language)) { cb(); return Disposable.NULL; } diff --git a/packages/navigator/package.json b/packages/navigator/package.json index ebff02fed78f7..9abadafa4f317 100644 --- a/packages/navigator/package.json +++ b/packages/navigator/package.json @@ -1,12 +1,13 @@ { "name": "@theia/navigator", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Navigator Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/workspace": "1.44.0", - "minimatch": "^5.1.0" + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/workspace": "1.54.0", + "minimatch": "^5.1.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -44,7 +45,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/navigator/src/browser/abstract-navigator-tree-widget.ts b/packages/navigator/src/browser/abstract-navigator-tree-widget.ts index 73c5898bea0ff..56d7962326ebd 100644 --- a/packages/navigator/src/browser/abstract-navigator-tree-widget.ts +++ b/packages/navigator/src/browser/abstract-navigator-tree-widget.ts @@ -16,7 +16,6 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { FileNavigatorPreferences } from './navigator-preferences'; -import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; import { FileTreeWidget } from '@theia/filesystem/lib/browser'; import { Attributes, HTMLAttributes } from '@theia/core/shared/react'; import { TreeNode } from '@theia/core/lib/browser'; @@ -24,9 +23,6 @@ import { TreeNode } from '@theia/core/lib/browser'; @injectable() export class AbstractNavigatorTreeWidget extends FileTreeWidget { - @inject(PreferenceService) - protected readonly preferenceService: PreferenceService; - @inject(FileNavigatorPreferences) protected readonly navigatorPreferences: FileNavigatorPreferences; diff --git a/packages/navigator/src/browser/file-navigator-commands.ts b/packages/navigator/src/browser/file-navigator-commands.ts index fa1c7c3b07edc..aefda94eb0fb2 100644 --- a/packages/navigator/src/browser/file-navigator-commands.ts +++ b/packages/navigator/src/browser/file-navigator-commands.ts @@ -52,11 +52,12 @@ export namespace FileNavigatorCommands { category: CommonCommands.FILE_CATEGORY, label: 'Focus on Files Explorer' }); - export const OPEN = Command.toDefaultLocalizedCommand({ + export const OPEN: Command = { id: 'navigator.open', - category: CommonCommands.FILE_CATEGORY, - label: 'Open' - }); + }; + export const OPEN_WITH: Command = { + id: 'navigator.openWith', + }; export const NEW_FILE_TOOLBAR: Command = { id: `${WorkspaceCommands.NEW_FILE.id}.toolbar`, iconClass: codicon('new-file') diff --git a/packages/navigator/src/browser/navigator-context-key-service.ts b/packages/navigator/src/browser/navigator-context-key-service.ts index e8c7fd8b0d666..df741f240cdc4 100644 --- a/packages/navigator/src/browser/navigator-context-key-service.ts +++ b/packages/navigator/src/browser/navigator-context-key-service.ts @@ -45,12 +45,22 @@ export class NavigatorContextKeyService { return this._explorerResourceIsFolder; } + protected _isFileSystemResource: ContextKey; + + /** + * True when the Explorer or editor file is a file system resource that can be handled from a file system provider. + */ + get isFileSystemResource(): ContextKey { + return this._isFileSystemResource; + } + @postConstruct() protected init(): void { this._explorerViewletVisible = this.contextKeyService.createKey('explorerViewletVisible', false); this._explorerViewletFocus = this.contextKeyService.createKey('explorerViewletFocus', false); this._filesExplorerFocus = this.contextKeyService.createKey('filesExplorerFocus', false); this._explorerResourceIsFolder = this.contextKeyService.createKey('explorerResourceIsFolder', false); + this._isFileSystemResource = this.contextKeyService.createKey('isFileSystemResource', false); } } diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index 2cf9ab6fa55a4..956436fbafe2a 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -14,7 +14,7 @@ // 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 { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify'; import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; import { CommonCommands, @@ -31,7 +31,8 @@ import { ApplicationShell, TabBar, Title, - SHELL_TABBAR_CONTEXT_MENU + SHELL_TABBAR_CONTEXT_MENU, + OpenWithService } from '@theia/core/lib/browser'; import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution'; import { @@ -40,6 +41,7 @@ import { MenuModelRegistry, MenuPath, Mutable, + QuickInputService, } from '@theia/core/lib/common'; import { DidCreateNewResourceEvent, @@ -55,8 +57,8 @@ import { FileNavigatorFilter } from './navigator-filter'; import { WorkspaceNode } from './navigator-tree'; import { NavigatorContextKeyService } from './navigator-context-key-service'; import { + RenderedToolbarItem, TabBarToolbarContribution, - TabBarToolbarItem, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; @@ -113,6 +115,7 @@ export namespace NavigatorContextMenu { /** @deprecated use MODIFICATION */ export const ACTIONS = MODIFICATION; + /** @deprecated use the `FileNavigatorCommands.OPEN_WITH` command */ export const OPEN_WITH = [...NAVIGATION, 'open_with']; } @@ -148,6 +151,12 @@ export class FileNavigatorContribution extends AbstractViewContribution this.openWithService.getHandlers(uri).length > 0, + isVisible: uri => this.openWithService.getHandlers(uri).length > 0, + execute: uri => this.openWithService.openWith(uri) + })); registry.registerCommand(OpenEditorsCommands.CLOSE_ALL_TABS_FROM_TOOLBAR, { execute: widget => this.withOpenEditorsWidget(widget, () => this.shell.closeMany(this.editorWidgets)), isEnabled: widget => this.withOpenEditorsWidget(widget, () => true), @@ -366,18 +380,12 @@ export class FileNavigatorContribution extends AbstractViewContribution { - for (const opener of openers) { - const openWithCommand = WorkspaceCommands.FILE_OPEN_WITH(opener); - registry.registerMenuAction(NavigatorContextMenu.OPEN_WITH, { - commandId: openWithCommand.id, - label: opener.label, - icon: opener.iconClass - }); - } + label: nls.localizeByDefault('Open') + }); + registry.registerMenuAction(NavigatorContextMenu.NAVIGATION, { + commandId: FileNavigatorCommands.OPEN_WITH.id, + when: '!explorerResourceIsFolder', + label: nls.localizeByDefault('Open With...') }); registry.registerMenuAction(NavigatorContextMenu.CLIPBOARD, { @@ -580,7 +588,7 @@ export class FileNavigatorContribution extends AbstractViewContribution) => { + public registerMoreToolbarItem = (item: Mutable & { command: string }) => { const commandId = item.command; const id = 'navigator.tabbar.toolbar.' + commandId; const command = this.commandRegistry.getCommand(commandId); diff --git a/packages/navigator/src/browser/navigator-diff.spec.ts b/packages/navigator/src/browser/navigator-diff.spec.ts index ced5d792c6678..2f70af9ace45f 100644 --- a/packages/navigator/src/browser/navigator-diff.spec.ts +++ b/packages/navigator/src/browser/navigator-diff.spec.ts @@ -31,7 +31,7 @@ import { OpenerService } from '@theia/core/lib/browser'; import { MockOpenerService } from '@theia/core/lib/browser/test/mock-opener-service'; import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageClient } from '@theia/core/lib/common/message-service-protocol'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { DiskFileSystemProvider } from '@theia/filesystem/lib/node/disk-file-system-provider'; diff --git a/packages/navigator/src/browser/navigator-widget.tsx b/packages/navigator/src/browser/navigator-widget.tsx index bf193fd0572de..e8d2b0a9f6cb7 100644 --- a/packages/navigator/src/browser/navigator-widget.tsx +++ b/packages/navigator/src/browser/navigator-widget.tsx @@ -19,7 +19,7 @@ import { Message } from '@theia/core/shared/@phosphor/messaging'; import URI from '@theia/core/lib/common/uri'; import { CommandService } from '@theia/core/lib/common'; import { Key, TreeModel, ContextMenuRenderer, ExpandableTreeNode, TreeProps, TreeNode } from '@theia/core/lib/browser'; -import { DirNode } from '@theia/filesystem/lib/browser'; +import { DirNode, FileStatNodeData } from '@theia/filesystem/lib/browser'; import { WorkspaceService, WorkspaceCommands } from '@theia/workspace/lib/browser'; import { WorkspaceNode, WorkspaceRootNode } from './navigator-tree'; import { FileNavigatorModel } from './navigator-model'; @@ -210,6 +210,10 @@ export class FileNavigatorWidget extends AbstractNavigatorTreeWidget { protected updateSelectionContextKeys(): void { this.contextKeyService.explorerResourceIsFolder.set(DirNode.is(this.model.selectedNodes[0])); + // As `FileStatNode` only created if `FileService.resolve` was successful, we can safely assume that + // a valid `FileSystemProvider` is available for the selected node. So we skip an additional check + // for provider availability here and check the node type. + this.contextKeyService.isFileSystemResource.set(FileStatNodeData.is(this.model.selectedNodes[0])); } } diff --git a/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts b/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts index 6e12555784473..3020bd5f4c335 100644 --- a/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts +++ b/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts @@ -14,17 +14,19 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Command, CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService } from '@theia/core'; -import { CommonCommands, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; +import { Command, CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService, URI } from '@theia/core'; +import { CommonCommands, KeybindingContribution, KeybindingRegistry, OpenWithService } from '@theia/core/lib/browser'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; +import { nls } from '@theia/core/lib/common'; +import { FileUri } from '@theia/core/lib/common/file-uri'; +import { isOSX, isWindows } from '@theia/core/lib/common/os'; +import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; +import '@theia/core/lib/electron-common/electron-api'; import { inject, injectable } from '@theia/core/shared/inversify'; import { FileStatNode } from '@theia/filesystem/lib/browser'; -import { FileNavigatorWidget, FILE_NAVIGATOR_ID } from '../browser'; -import { NavigatorContextMenu, SHELL_TABBAR_CONTEXT_REVEAL } from '../browser/navigator-contribution'; -import { isWindows, isOSX } from '@theia/core/lib/common/os'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; -import '@theia/core/lib/electron-common/electron-api'; +import { FILE_NAVIGATOR_ID, FileNavigatorWidget } from '../browser'; +import { NavigatorContextMenu, SHELL_TABBAR_CONTEXT_REVEAL } from '../browser/navigator-contribution'; export const OPEN_CONTAINING_FOLDER = Command.toDefaultLocalizedCommand({ id: 'revealFileInOS', @@ -34,6 +36,12 @@ export const OPEN_CONTAINING_FOLDER = Command.toDefaultLocalizedCommand({ /* linux */ 'Open Containing Folder' }); +export const OPEN_WITH_SYSTEM_APP = Command.toDefaultLocalizedCommand({ + id: 'openWithSystemApp', + category: CommonCommands.FILE_CATEGORY, + label: 'Open With System Editor' +}); + @injectable() export class ElectronNavigatorMenuContribution implements MenuContribution, CommandContribution, KeybindingContribution { @@ -46,14 +54,37 @@ export class ElectronNavigatorMenuContribution implements MenuContribution, Comm @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(OpenWithService) + protected readonly openWithService: OpenWithService; + registerCommands(commands: CommandRegistry): void { commands.registerCommand(OPEN_CONTAINING_FOLDER, UriAwareCommandHandler.MonoSelect(this.selectionService, { execute: async uri => { - window.electronTheiaCore.showItemInFolder(uri['codeUri'].fsPath); + window.electronTheiaCore.showItemInFolder(FileUri.fsPath(uri)); }, isEnabled: uri => !!this.workspaceService.getWorkspaceRootUri(uri), isVisible: uri => !!this.workspaceService.getWorkspaceRootUri(uri), })); + commands.registerCommand(OPEN_WITH_SYSTEM_APP, UriAwareCommandHandler.MonoSelect(this.selectionService, { + execute: async uri => { + this.openWithSystemApplication(uri); + } + })); + this.openWithService.registerHandler({ + id: 'system-editor', + label: nls.localize('theia/navigator/systemEditor', 'System Editor'), + providerName: nls.localizeByDefault('Built-in'), + // Low priority to avoid conflicts with other open handlers. + canHandle: uri => (uri.scheme === 'file') ? 10 : 0, + open: uri => { + this.openWithSystemApplication(uri); + return {}; + } + }); + } + + protected openWithSystemApplication(uri: URI): void { + window.electronTheiaCore.openWithSystemApp(FileUri.fsPath(uri)); } registerMenus(menus: MenuModelRegistry): void { diff --git a/packages/notebook/package.json b/packages/notebook/package.json index 56914c2e284fe..cc15cf13ac99f 100644 --- a/packages/notebook/package.json +++ b/packages/notebook/package.json @@ -1,13 +1,17 @@ { "name": "@theia/notebook", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Notebook Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/monaco": "1.44.0", - "uuid": "^8.3.2" + "@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/outline-view": "1.54.0", + "advanced-mark.js": "^2.6.0", + "react-perfect-scrollbar": "^1.5.8", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -42,7 +46,8 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0", + "@theia/ext-scripts": "1.54.0", + "@types/markdown-it": "^12.2.3", "@types/vscode-notebook-renderer": "^1.72.0" }, "nyc": { diff --git a/packages/notebook/src/browser/contributions/cell-operations.ts b/packages/notebook/src/browser/contributions/cell-operations.ts new file mode 100644 index 0000000000000..1953be0527319 --- /dev/null +++ b/packages/notebook/src/browser/contributions/cell-operations.ts @@ -0,0 +1,44 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { CellEditType, CellKind } from '../../common'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { NotebookModel } from '../view-model/notebook-model'; + +/** + * a collection of different reusable notbook cell operations + */ + +export function changeCellType(notebookModel: NotebookModel, cell: NotebookCellModel, type: CellKind, language?: string): void { + if (cell.cellKind === type) { + return; + } + if (type === CellKind.Markup) { + language = 'markdown'; + } else { + language ??= cell.language; + } + notebookModel.applyEdits([{ + editType: CellEditType.Replace, + index: notebookModel.cells.indexOf(cell), + count: 1, + cells: [{ + ...cell.getData(), + cellKind: type, + language + }] + }], true); +} diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts index abd9516f3b346..3a95c0c644d6b 100644 --- a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -14,14 +14,18 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Command, CommandContribution, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; +import { Command, CommandContribution, CommandHandler, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { ApplicationShell, codicon, CommonCommands } from '@theia/core/lib/browser'; +import { ApplicationShell, codicon, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookService } from '../service/notebook-service'; -import { CellEditType, CellKind } from '../../common'; -import { KernelPickerMRUStrategy, NotebookKernelQuickPickService } from '../service/notebook-kernel-quick-pick-service'; +import { CellEditType, CellKind, NotebookCommand } from '../../common'; +import { NotebookKernelQuickPickService } from '../service/notebook-kernel-quick-pick-service'; import { NotebookExecutionService } from '../service/notebook-execution-service'; +import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; +import { NOTEBOOK_CELL_CURSOR_FIRST_LINE, NOTEBOOK_CELL_CURSOR_LAST_LINE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS } from './notebook-context-keys'; +import { NotebookClipboardService } from '../service/notebook-clipboard-service'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { NotebookEditorWidget } from '../notebook-editor-widget'; export namespace NotebookCommands { @@ -32,13 +36,15 @@ export namespace NotebookCommands { export const ADD_NEW_MARKDOWN_CELL_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.add-new-markdown-cell', - iconClass: codicon('add') - }); + iconClass: codicon('add'), + tooltip: nls.localizeByDefault('Add Markdown Cell') + } as NotebookCommand); export const ADD_NEW_CODE_CELL_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.add-new-code-cell', - iconClass: codicon('add') - }); + iconClass: codicon('add'), + tooltip: nls.localizeByDefault('Add Code Cell') + } as NotebookCommand); export const SELECT_KERNEL_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.selectKernel', @@ -57,16 +63,51 @@ export namespace NotebookCommands { category: 'Notebook', iconClass: codicon('clear-all') }); + + export const CHANGE_SELECTED_CELL = Command.toDefaultLocalizedCommand({ + id: 'notebook.change-selected-cell', + category: 'Notebook', + }); + + export const CUT_SELECTED_CELL = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.cut', + category: 'Notebook', + }); + + export const COPY_SELECTED_CELL = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.copy', + category: 'Notebook', + }); + + export const PASTE_CELL = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.paste', + category: 'Notebook', + }); + + export const NOTEBOOK_FIND = Command.toDefaultLocalizedCommand({ + id: 'notebook.find', + category: 'Notebook', + }); + + export const CENTER_ACTIVE_CELL = Command.toDefaultLocalizedCommand({ + id: 'notebook.centerActiveCell', + category: 'Notebook', + }); +} + +export enum CellChangeDirection { + Up = 'up', + Down = 'down' } @injectable() -export class NotebookActionsContribution implements CommandContribution, MenuContribution { +export class NotebookActionsContribution implements CommandContribution, MenuContribution, KeybindingContribution { @inject(NotebookService) protected notebookService: NotebookService; @inject(NotebookKernelQuickPickService) - protected notebookKernelQuickPickService: KernelPickerMRUStrategy; + protected notebookKernelQuickPickService: NotebookKernelQuickPickService; @inject(NotebookExecutionService) protected notebookExecutionService: NotebookExecutionService; @@ -74,13 +115,31 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon @inject(ApplicationShell) protected shell: ApplicationShell; + @inject(NotebookEditorWidgetService) + protected notebookEditorWidgetService: NotebookEditorWidgetService; + + @inject(NotebookClipboardService) + protected notebookClipboardService: NotebookClipboardService; + + @inject(ContextKeyService) + protected contextKeyService: ContextKeyService; + registerCommands(commands: CommandRegistry): void { commands.registerCommand(NotebookCommands.ADD_NEW_CELL_COMMAND, { - execute: (notebookModel: NotebookModel, cellKind: CellKind, index?: number) => { - const insertIndex = index ?? (notebookModel.selectedCell ? notebookModel.cells.indexOf(notebookModel.selectedCell) : 0); - let firstCodeCell; + execute: (notebookModel: NotebookModel, cellKind: CellKind = CellKind.Markup, index?: number | 'above' | 'below', focusContainer?: boolean) => { + notebookModel = notebookModel ?? this.notebookEditorWidgetService.focusedEditor?.model; + + let insertIndex: number = 0; + if (typeof index === 'number' && index >= 0) { + insertIndex = index; + } else if (notebookModel.selectedCell && typeof index === 'string') { + // if index is -1 insert below otherwise at the index of the selected cell which is above the selected. + insertIndex = notebookModel.cells.indexOf(notebookModel.selectedCell) + (index === 'below' ? 1 : 0); + } + + let cellLanguage: string = 'markdown'; if (cellKind === CellKind.Code) { - firstCodeCell = notebookModel.cells.find(cell => cell.cellKind === CellKind.Code); + cellLanguage = this.notebookService.getCodeCellLanguage(notebookModel); } notebookModel.applyEdits([{ @@ -89,44 +148,148 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon count: 0, cells: [{ cellKind, - language: firstCodeCell?.language ?? 'markdown', + language: cellLanguage, source: '', outputs: [], metadata: {}, }] }], true); + if (focusContainer) { + notebookModel.selectedCell?.requestBlurEditor(); + } } }); - commands.registerCommand(NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND, { - execute: (notebookModel: NotebookModel) => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Markup) - }); + commands.registerCommand(NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND, this.editableCommandHandler( + notebookModel => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Markup, 'below') + )); - commands.registerCommand(NotebookCommands.ADD_NEW_CODE_CELL_COMMAND, { - execute: (notebookModel: NotebookModel) => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Code) - }); + commands.registerCommand(NotebookCommands.ADD_NEW_CODE_CELL_COMMAND, this.editableCommandHandler( + notebookModel => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Code, 'below') + )); + + commands.registerCommand(NotebookCommands.SELECT_KERNEL_COMMAND, this.editableCommandHandler( + notebookModel => this.notebookKernelQuickPickService.showQuickPick(notebookModel) + )); + + commands.registerCommand(NotebookCommands.EXECUTE_NOTEBOOK_COMMAND, this.editableCommandHandler( + notebookModel => this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells) + )); - commands.registerCommand(NotebookCommands.SELECT_KERNEL_COMMAND, { - execute: (notebookModel: NotebookModel) => this.notebookKernelQuickPickService.showQuickPick(notebookModel) + commands.registerCommand(NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND, this.editableCommandHandler( + notebookModel => notebookModel.applyEdits(notebookModel.cells.map(cell => ({ + editType: CellEditType.Output, + handle: cell.handle, deleteCount: cell.outputs.length, outputs: [] + })), false) + )); + + commands.registerCommand(NotebookCommands.CHANGE_SELECTED_CELL, + { + execute: (change: number | CellChangeDirection) => { + const focusedEditor = this.notebookEditorWidgetService.focusedEditor; + const model = focusedEditor?.model; + if (model && typeof change === 'number') { + model.setSelectedCell(model.cells[change]); + } else if (model && model.selectedCell) { + const currentIndex = model.cells.indexOf(model.selectedCell); + const shouldFocusEditor = this.contextKeyService.match('editorTextFocus'); + + if (change === CellChangeDirection.Up && currentIndex > 0) { + model.setSelectedCell(model.cells[currentIndex - 1]); + if ((model.selectedCell?.cellKind === CellKind.Code + || (model.selectedCell?.cellKind === CellKind.Markup && model.selectedCell?.editing)) && shouldFocusEditor) { + model.selectedCell.requestFocusEditor('lastLine'); + } + } else if (change === CellChangeDirection.Down && currentIndex < model.cells.length - 1) { + model.setSelectedCell(model.cells[currentIndex + 1]); + if ((model.selectedCell?.cellKind === CellKind.Code + || (model.selectedCell?.cellKind === CellKind.Markup && model.selectedCell?.editing)) && shouldFocusEditor) { + model.selectedCell.requestFocusEditor(); + } + } + + if (model.selectedCell.cellKind === CellKind.Markup) { + // since were losing focus from the cell editor, we need to focus the notebook editor again + focusedEditor?.node.focus(); + } + } + } + } + ); + commands.registerCommand({ id: 'list.focusUp' }, { + execute: () => commands.executeCommand(NotebookCommands.CHANGE_SELECTED_CELL.id, CellChangeDirection.Up) + }); + commands.registerCommand({ id: 'list.focusDown' }, { + execute: () => commands.executeCommand(NotebookCommands.CHANGE_SELECTED_CELL.id, CellChangeDirection.Down) }); - commands.registerCommand(NotebookCommands.EXECUTE_NOTEBOOK_COMMAND, { - execute: (notebookModel: NotebookModel) => this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells) + commands.registerCommand(NotebookCommands.CUT_SELECTED_CELL, this.editableCommandHandler( + () => { + const model = this.notebookEditorWidgetService.focusedEditor?.model; + const selectedCell = model?.selectedCell; + if (selectedCell) { + model.applyEdits([{ editType: CellEditType.Replace, index: model.cells.indexOf(selectedCell), count: 1, cells: [] }], true); + this.notebookClipboardService.copyCell(selectedCell); + } + })); + + commands.registerCommand(NotebookCommands.COPY_SELECTED_CELL, { + execute: () => { + const model = this.notebookEditorWidgetService.focusedEditor?.model; + const selectedCell = model?.selectedCell; + if (selectedCell) { + this.notebookClipboardService.copyCell(selectedCell); + } + } }); - commands.registerCommand(NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND, { - execute: (notebookModel: NotebookModel) => - notebookModel.cells.forEach(cell => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] })) + commands.registerCommand(NotebookCommands.PASTE_CELL, { + isEnabled: () => !Boolean(this.notebookEditorWidgetService.focusedEditor?.model?.readOnly), + isVisible: () => !Boolean(this.notebookEditorWidgetService.focusedEditor?.model?.readOnly), + execute: (position?: 'above') => { + const copiedCell = this.notebookClipboardService.getCell(); + if (copiedCell) { + const model = this.notebookEditorWidgetService.focusedEditor?.model; + const insertIndex = model?.selectedCell ? model.cells.indexOf(model.selectedCell) + (position === 'above' ? 0 : 1) : 0; + model?.applyEdits([{ editType: CellEditType.Replace, index: insertIndex, count: 0, cells: [copiedCell] }], true); + } + } }); - commands.registerHandler(CommonCommands.UNDO.id, { - isEnabled: () => this.shell.activeWidget instanceof NotebookEditorWidget, - execute: () => (this.shell.activeWidget as NotebookEditorWidget).undo() + commands.registerCommand(NotebookCommands.NOTEBOOK_FIND, { + execute: () => { + this.notebookEditorWidgetService.focusedEditor?.showFindWidget(); + } }); - commands.registerHandler(CommonCommands.REDO.id, { - isEnabled: () => this.shell.activeWidget instanceof NotebookEditorWidget, - execute: () => (this.shell.activeWidget as NotebookEditorWidget).redo() + + commands.registerCommand(NotebookCommands.CENTER_ACTIVE_CELL, { + execute: (editor?: NotebookEditorWidget) => { + const model = editor ? editor.model : this.notebookEditorWidgetService.focusedEditor?.model; + model?.selectedCell?.requestCenterEditor(); + } }); + + } + + protected editableCommandHandler(execute: (notebookModel: NotebookModel) => void): CommandHandler { + return { + isEnabled: (item: URI | NotebookModel) => this.withModel(item, model => !Boolean(model?.readOnly), false), + isVisible: (item: URI | NotebookModel) => this.withModel(item, model => !Boolean(model?.readOnly), false), + execute: (uri: URI | NotebookModel) => { + this.withModel(uri, execute, undefined); + } + }; + } + + protected withModel(item: URI | NotebookModel, execute: (notebookModel: NotebookModel) => T, defaultValue: T): T { + if (item instanceof URI) { + const model = this.notebookService.getNotebookEditorModel(item); + if (!model) { + return defaultValue; + } + item = model; + } + return execute(item); } registerMenus(menus: MenuModelRegistry): void { @@ -157,9 +320,53 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon commandId: NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND.id, label: nls.localizeByDefault('Clear All Outputs'), icon: codicon('clear-all'), - order: '30' + order: '30', + when: NOTEBOOK_HAS_OUTPUTS }); - // other items + + menus.registerIndependentSubmenu(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU, ''); + } + + registerKeybindings(keybindings: KeybindingRegistry): void { + keybindings.registerKeybindings( + { + command: NotebookCommands.CHANGE_SELECTED_CELL.id, + keybinding: 'up', + args: CellChangeDirection.Up, + when: `(!editorTextFocus || ${NOTEBOOK_CELL_CURSOR_FIRST_LINE}) && !suggestWidgetVisible && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}` + }, + { + command: NotebookCommands.CHANGE_SELECTED_CELL.id, + keybinding: 'down', + args: CellChangeDirection.Down, + when: `(!editorTextFocus || ${NOTEBOOK_CELL_CURSOR_LAST_LINE}) && !suggestWidgetVisible && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}` + }, + { + command: NotebookCommands.CUT_SELECTED_CELL.id, + keybinding: 'ctrlcmd+x', + when: `${NOTEBOOK_EDITOR_FOCUSED} && !inputFocus` + }, + { + command: NotebookCommands.COPY_SELECTED_CELL.id, + keybinding: 'ctrlcmd+c', + when: `${NOTEBOOK_EDITOR_FOCUSED} && !inputFocus` + }, + { + command: NotebookCommands.PASTE_CELL.id, + keybinding: 'ctrlcmd+v', + when: `${NOTEBOOK_EDITOR_FOCUSED} && !inputFocus` + }, + { + command: NotebookCommands.NOTEBOOK_FIND.id, + keybinding: 'ctrlcmd+f', + when: `${NOTEBOOK_EDITOR_FOCUSED}` + }, + { + command: NotebookCommands.CENTER_ACTIVE_CELL.id, + keybinding: 'ctrlcmd+l', + when: `${NOTEBOOK_EDITOR_FOCUSED}` + } + ); } } @@ -168,4 +375,5 @@ export namespace NotebookMenus { export const NOTEBOOK_MAIN_TOOLBAR = 'notebook/toolbar'; export const NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP = [NOTEBOOK_MAIN_TOOLBAR, 'cell-add-group']; export const NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP = [NOTEBOOK_MAIN_TOOLBAR, 'cell-execution-group']; + export const NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU = 'notebook-main-toolbar-hidden-items-context-menu'; } diff --git a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts index 39071c329623d..38c50a753c21a 100644 --- a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts @@ -14,26 +14,41 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Command, CommandContribution, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; -import { codicon } from '@theia/core/lib/browser'; +import { Command, CommandContribution, CommandHandler, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; +import { codicon, Key, KeybindingContribution, KeybindingRegistry, KeyCode, KeyModifier } from '@theia/core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; -import { NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NotebookContextKeys, NOTEBOOK_CELL_EXECUTING } from './notebook-context-keys'; +import { + NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, + NotebookContextKeys, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_EDITOR_FOCUSED, + NOTEBOOK_CELL_FOCUSED, + NOTEBOOK_CELL_LIST_FOCUSED +} from './notebook-context-keys'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { NotebookExecutionService } from '../service/notebook-execution-service'; import { NotebookCellOutputModel } from '../view-model/notebook-cell-output-model'; -import { CellEditType } from '../../common'; +import { CellData, CellEditType, CellKind } from '../../common'; +import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; +import { NotebookCommands } from './notebook-actions-contribution'; +import { changeCellType } from './cell-operations'; +import { EditorLanguageQuickPickService } from '@theia/editor/lib/browser/editor-language-quick-pick-service'; +import { NotebookService } from '../service/notebook-service'; +import { Selection } from '@theia/monaco-editor-core/esm/vs/editor/common/core/selection'; +import { Range } from '@theia/core/shared/vscode-languageserver-protocol'; +import { NOTEBOOK_EDITOR_ID_PREFIX } from '../notebook-editor-widget'; export namespace NotebookCellCommands { /** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel */ export const EDIT_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.cell.edit', + category: 'Notebook', iconClass: codicon('edit') }); /** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel */ export const STOP_EDIT_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.cell.stop-edit', + category: 'Notebook', iconClass: codicon('check') }); /** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */ @@ -43,15 +58,41 @@ export namespace NotebookCellCommands { }); /** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */ export const SPLIT_CELL_COMMAND = Command.toDefaultLocalizedCommand({ - id: 'notebook.cell.split-cell', + id: 'notebook.cell.split', iconClass: codicon('split-vertical'), }); /** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */ export const EXECUTE_SINGLE_CELL_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.cell.execute-cell', + category: 'Notebook', + label: nls.localizeByDefault('Execute Cell'), iconClass: codicon('play'), }); /** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */ + export const EXECUTE_SINGLE_CELL_AND_FOCUS_NEXT_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.execute-cell-and-focus-next', + label: nls.localizeByDefault('Execute Notebook Cell and Select Below'), + category: 'Notebook', + }); + /** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */ + export const EXECUTE_SINGLE_CELL_AND_INSERT_BELOW_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.execute-cell-and-insert-below', + label: nls.localizeByDefault('Execute Notebook Cell and Insert Below'), + category: 'Notebook', + }); + + export const EXECUTE_ABOVE_CELLS_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebookActions.executeAbove', + label: 'Execute Above Cells', + iconClass: codicon('run-above') + }); + + export const EXECUTE_CELL_AND_BELOW_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebookActions.executeBelow', + label: 'Execute Cell and Below', + iconClass: codicon('run-below') + }); + /** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */ export const STOP_CELL_EXECUTION_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.cell.stop-cell-execution', iconClass: codicon('stop'), @@ -59,24 +100,85 @@ export namespace NotebookCellCommands { /** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel */ export const CLEAR_OUTPUTS_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.cell.clear-outputs', + category: 'Notebook', label: 'Clear Cell Outputs', }); /** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel | undefined, output: NotebookCellOutputModel */ export const CHANGE_OUTPUT_PRESENTATION_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.cell.change-presentation', + category: 'Notebook', label: 'Change Presentation', }); + + export const INSERT_NEW_CELL_ABOVE_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.insertCodeCellAboveAndFocusContainer', + label: 'Insert Code Cell Above and Focus Container' + }); + + export const INSERT_NEW_CELL_BELOW_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.insertCodeCellBelowAndFocusContainer', + label: 'Insert Code Cell Below and Focus Container' + }); + + export const INSERT_MARKDOWN_CELL_ABOVE_COMMAND = Command.toLocalizedCommand({ + id: 'notebook.cell.insertMarkdownCellAbove', + label: 'Insert Markdown Cell Above' + }); + export const INSERT_MARKDOWN_CELL_BELOW_COMMAND = Command.toLocalizedCommand({ + id: 'notebook.cell.insertMarkdownCellBelow', + label: 'Insert Markdown Cell Below' + }); + + export const TO_CODE_CELL_COMMAND = Command.toLocalizedCommand({ + id: 'notebook.cell.changeToCode', + category: 'Notebook', + label: 'Change Cell to Code' + }); + + export const TO_MARKDOWN_CELL_COMMAND = Command.toLocalizedCommand({ + id: 'notebook.cell.changeToMarkdown', + category: 'Notebook', + label: 'Change Cell to Markdown' + }); + + export const TOGGLE_CELL_OUTPUT = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.toggleOutputs', + category: 'Notebook', + label: 'Collapse Cell Output', + }); + + export const CHANGE_CELL_LANGUAGE = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.changeLanguage', + category: 'Notebook', + label: 'Change Cell Language', + }); + + export const TOGGLE_LINE_NUMBERS = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.toggleLineNumbers', + category: 'Notebook', + label: 'Show Cell Line Numbers', + }); + } @injectable() -export class NotebookCellActionContribution implements MenuContribution, CommandContribution { +export class NotebookCellActionContribution implements MenuContribution, CommandContribution, KeybindingContribution { @inject(ContextKeyService) protected contextKeyService: ContextKeyService; + @inject(NotebookService) + protected notebookService: NotebookService; + @inject(NotebookExecutionService) protected notebookExecutionService: NotebookExecutionService; + @inject(NotebookEditorWidgetService) + protected notebookEditorWidgetService: NotebookEditorWidgetService; + + @inject(EditorLanguageQuickPickService) + protected languageQuickPickService: EditorLanguageQuickPickService; + @postConstruct() protected init(): void { NotebookContextKeys.initNotebookContextKeys(this.contextKeyService); @@ -97,20 +199,30 @@ export class NotebookCellActionContribution implements MenuContribution, Command label: nls.localizeByDefault('Stop Editing Cell'), order: '10' }); + menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, { - commandId: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, - icon: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.iconClass, + commandId: NotebookCellCommands.EXECUTE_ABOVE_CELLS_COMMAND.id, + icon: NotebookCellCommands.EXECUTE_ABOVE_CELLS_COMMAND.iconClass, when: `${NOTEBOOK_CELL_TYPE} == 'code'`, - label: nls.localizeByDefault('Execute Cell'), + label: nls.localizeByDefault('Execute Above Cells'), order: '10' }); + menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, { + commandId: NotebookCellCommands.EXECUTE_CELL_AND_BELOW_COMMAND.id, + icon: NotebookCellCommands.EXECUTE_CELL_AND_BELOW_COMMAND.iconClass, + when: `${NOTEBOOK_CELL_TYPE} == 'code'`, + label: nls.localizeByDefault('Execute Cell and Below'), + order: '20' + }); + menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, { commandId: NotebookCellCommands.SPLIT_CELL_COMMAND.id, icon: NotebookCellCommands.SPLIT_CELL_COMMAND.iconClass, label: nls.localizeByDefault('Split Cell'), order: '20' }); + menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, { commandId: NotebookCellCommands.DELETE_COMMAND.id, icon: NotebookCellCommands.DELETE_COMMAND.iconClass, @@ -150,7 +262,7 @@ export class NotebookCellActionContribution implements MenuContribution, Command menus.registerIndependentSubmenu(NotebookCellActionContribution.CONTRIBUTED_CELL_EXECUTION_MENU, nls.localizeByDefault('More...'), { role: CompoundMenuNodeRole.Flat, icon: codicon('chevron-down') }); - menus.getMenu(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU).addNode(menus.getMenuNode(NotebookCellActionContribution.CONTRIBUTED_CELL_EXECUTION_MENU)); + // menus.getMenu(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU).addNode(menus.getMenuNode(NotebookCellActionContribution.CONTRIBUTED_CELL_EXECUTION_MENU)); // code cell output sidebar menu menus.registerSubmenu( @@ -172,30 +284,281 @@ export class NotebookCellActionContribution implements MenuContribution, Command } registerCommands(commands: CommandRegistry): void { - commands.registerCommand(NotebookCellCommands.EDIT_COMMAND, { execute: (_, cell: NotebookCellModel) => cell.requestEdit() }); - commands.registerCommand(NotebookCellCommands.STOP_EDIT_COMMAND, { execute: (_, cell: NotebookCellModel) => cell.requestStopEdit() }); - commands.registerCommand(NotebookCellCommands.DELETE_COMMAND, { - execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => notebookModel.applyEdits([{ - editType: CellEditType.Replace, - index: notebookModel.cells.indexOf(cell), - count: 1, - cells: [] - }], true) - }); - commands.registerCommand(NotebookCellCommands.SPLIT_CELL_COMMAND); + commands.registerCommand(NotebookCellCommands.EDIT_COMMAND, this.editableCellCommandHandler((_, cell) => cell.requestFocusEditor())); + commands.registerCommand(NotebookCellCommands.STOP_EDIT_COMMAND, { execute: (_, cell: NotebookCellModel) => (cell ?? this.getSelectedCell()).requestBlurEditor() }); + commands.registerCommand(NotebookCellCommands.DELETE_COMMAND, + this.editableCellCommandHandler((notebookModel, cell) => { + notebookModel.applyEdits([{ + editType: CellEditType.Replace, + index: notebookModel.cells.indexOf(cell), + count: 1, + cells: [] + }] + , true); + })); + commands.registerCommand(NotebookCellCommands.SPLIT_CELL_COMMAND, this.editableCellCommandHandler( + async (notebookModel, cell) => { + // selection (0,0,0,0) should also be used in !cell.editing mode, but `cell.editing` + // is not properly implemented for Code cells. + const cellSelection: Range = cell.selection ?? { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }; + const textModel = await cell.resolveTextModel(); + + // Create new cell with the text after the cursor + const splitOffset = textModel.offsetAt({ + line: cellSelection.start.line, + character: cellSelection.start.character + }); + const newCell: CellData = { + cellKind: cell.cellKind, + language: cell.language, + outputs: [], + source: textModel.getText().substring(splitOffset), + }; + + // add new cell below + const index = notebookModel.cells.indexOf(cell); + notebookModel.applyEdits([{ editType: CellEditType.Replace, index: index + 1, count: 0, cells: [newCell] }], true); + + // update current cell text (undo-able) + const selection = new Selection(cellSelection.start.line + 1, cellSelection.start.character + 1, cellSelection.end.line + 1, cellSelection.end.character + 1); + const endPosition = textModel.positionAt(textModel.getText().length); + const deleteOp = { + range: { + startLineNumber: selection.startLineNumber, + startColumn: selection.startColumn, + endLineNumber: endPosition.line + 1, + endColumn: endPosition.character + 1 + }, + // eslint-disable-next-line no-null/no-null + text: null + }; + // Create a new undo/redo stack entry + textModel.textEditorModel.pushStackElement(); + textModel.textEditorModel.pushEditOperations([selection], [deleteOp], () => [selection]); + }) + ); + + commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND, this.editableCellCommandHandler( + (notebookModel, cell) => { + this.notebookExecutionService.executeNotebookCells(notebookModel, [cell]); + }) + ); + + commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_FOCUS_NEXT_COMMAND, this.editableCellCommandHandler( + (notebookModel, cell) => { + if (cell.cellKind === CellKind.Code) { + commands.executeCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, notebookModel, cell); + } else { + commands.executeCommand(NotebookCellCommands.STOP_EDIT_COMMAND.id, notebookModel, cell); + } + const index = notebookModel.cells.indexOf(cell); + if (index < notebookModel.cells.length - 1) { + notebookModel.setSelectedCell(notebookModel.cells[index + 1]); + } else if (cell.cellKind === CellKind.Code) { + commands.executeCommand(NotebookCellCommands.INSERT_NEW_CELL_BELOW_COMMAND.id); + } else { + commands.executeCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_BELOW_COMMAND.id); + } + }) + ); + commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_INSERT_BELOW_COMMAND, this.editableCellCommandHandler( + async (notebookModel, cell) => { + if (cell.cellKind === CellKind.Code) { + await commands.executeCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, notebookModel, cell); + } + await commands.executeCommand(NotebookCellCommands.STOP_EDIT_COMMAND.id, notebookModel, cell); + + if (cell.cellKind === CellKind.Code) { + await commands.executeCommand(NotebookCellCommands.INSERT_NEW_CELL_BELOW_COMMAND.id); + } else { + await commands.executeCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_BELOW_COMMAND.id); + } + + const index = notebookModel.cells.indexOf(cell); + notebookModel.setSelectedCell(notebookModel.cells[index + 1]); + }) + ); + + commands.registerCommand(NotebookCellCommands.EXECUTE_ABOVE_CELLS_COMMAND, this.editableCellCommandHandler( + (notebookModel, cell) => { + const index = notebookModel.cells.indexOf(cell); + if (index > 0) { + this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells.slice(0, index).filter(c => c.cellKind === CellKind.Code)); + } + }) + ); + + commands.registerCommand(NotebookCellCommands.EXECUTE_CELL_AND_BELOW_COMMAND, this.editableCellCommandHandler( + (notebookModel, cell) => { + const index = notebookModel.cells.indexOf(cell); + if (index < notebookModel.cells.length - 1) { + this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells.slice(index).filter(c => c.cellKind === CellKind.Code)); + } + }) + ); - commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND, { - execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => this.notebookExecutionService.executeNotebookCells(notebookModel, [cell]) - }); commands.registerCommand(NotebookCellCommands.STOP_CELL_EXECUTION_COMMAND, { - execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => this.notebookExecutionService.cancelNotebookCells(notebookModel, [cell]) + execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => { + notebookModel = notebookModel ?? this.notebookEditorWidgetService.focusedEditor?.model; + cell = cell ?? this.getSelectedCell(); + this.notebookExecutionService.cancelNotebookCells(notebookModel, [cell]); + } }); - commands.registerCommand(NotebookCellCommands.CLEAR_OUTPUTS_COMMAND, { - execute: (_, cell: NotebookCellModel) => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] }) + commands.registerCommand(NotebookCellCommands.CLEAR_OUTPUTS_COMMAND, this.editableCellCommandHandler( + (notebook, cell) => (notebook ?? this.notebookEditorWidgetService.focusedEditor?.model)?.applyEdits([{ + editType: CellEditType.Output, + handle: cell.handle, + outputs: [], + deleteCount: cell.outputs.length, + append: false + }], true) + )); + commands.registerCommand(NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND, this.editableCellCommandHandler( + (notebook, cell, output) => { + this.notebookEditorWidgetService.getNotebookEditor(NOTEBOOK_EDITOR_ID_PREFIX + notebook.uri.toString())?.requestOuputPresentationChange(cell.handle, output); + } + )); + + const insertCommand = (type: CellKind, index: number | 'above' | 'below', focusContainer: boolean): CommandHandler => this.editableCellCommandHandler(() => + commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, undefined, type, index, focusContainer) + ); + commands.registerCommand(NotebookCellCommands.INSERT_NEW_CELL_ABOVE_COMMAND, insertCommand(CellKind.Code, 'above', true)); + commands.registerCommand(NotebookCellCommands.INSERT_NEW_CELL_BELOW_COMMAND, insertCommand(CellKind.Code, 'below', true)); + commands.registerCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_ABOVE_COMMAND, insertCommand(CellKind.Markup, 'above', false)); + commands.registerCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_BELOW_COMMAND, insertCommand(CellKind.Markup, 'below', false)); + + commands.registerCommand(NotebookCellCommands.TO_CODE_CELL_COMMAND, this.editableCellCommandHandler((notebookModel, cell) => { + changeCellType(notebookModel, cell, CellKind.Code, this.notebookService.getCodeCellLanguage(notebookModel)); + })); + commands.registerCommand(NotebookCellCommands.TO_MARKDOWN_CELL_COMMAND, this.editableCellCommandHandler((notebookModel, cell) => { + changeCellType(notebookModel, cell, CellKind.Markup); + })); + + commands.registerCommand(NotebookCellCommands.TOGGLE_CELL_OUTPUT, { + execute: () => { + const selectedCell = this.notebookEditorWidgetService.focusedEditor?.model?.selectedCell; + if (selectedCell) { + selectedCell.outputVisible = !selectedCell.outputVisible; + } + } }); - commands.registerCommand(NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND, { - execute: (_, __, output: NotebookCellOutputModel) => output.requestOutputPresentationUpdate() + + commands.registerCommand(NotebookCellCommands.CHANGE_CELL_LANGUAGE, { + isVisible: () => !!this.notebookEditorWidgetService.focusedEditor?.model?.selectedCell, + execute: async (notebook?: NotebookModel, cell?: NotebookCellModel) => { + const selectedCell = cell ?? this.notebookEditorWidgetService.focusedEditor?.model?.selectedCell; + const activeNotebook = notebook ?? this.notebookEditorWidgetService.focusedEditor?.model; + if (!selectedCell || !activeNotebook) { + return; + } + const language = await this.languageQuickPickService.pickEditorLanguage(selectedCell.language); + if (!language?.value || language.value === 'autoDetect' || language.value.id === selectedCell.language) { + return; + } + const isMarkdownCell = selectedCell.cellKind === CellKind.Markup; + const isMarkdownLanguage = language.value.id === 'markdown'; + if (isMarkdownLanguage) { + changeCellType(activeNotebook, selectedCell, CellKind.Markup, language.value.id); + } else { + if (isMarkdownCell) { + changeCellType(activeNotebook, selectedCell, CellKind.Code, language.value.id); + } else { + this.notebookEditorWidgetService.focusedEditor?.model?.applyEdits([{ + editType: CellEditType.CellLanguage, + index: activeNotebook.cells.indexOf(selectedCell), + language: language.value.id + }], true); + } + } + } + }); + + commands.registerCommand(NotebookCellCommands.TOGGLE_LINE_NUMBERS, { + execute: () => { + const selectedCell = this.notebookEditorWidgetService.focusedEditor?.model?.selectedCell; + if (selectedCell) { + const currentLineNumber = selectedCell.editorOptions?.lineNumbers; + selectedCell.editorOptions = { ...selectedCell.editorOptions, lineNumbers: !currentLineNumber || currentLineNumber === 'off' ? 'on' : 'off' }; + } + } }); + + } + + protected editableCellCommandHandler(execute: (notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel) => void): CommandHandler { + return { + isEnabled: (notebookModel: NotebookModel) => !Boolean(notebookModel?.readOnly), + isVisible: (notebookModel: NotebookModel) => !Boolean(notebookModel?.readOnly), + execute: (notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel) => { + notebookModel = notebookModel ?? this.notebookEditorWidgetService.focusedEditor?.model; + cell = cell ?? this.getSelectedCell(); + execute(notebookModel, cell, output); + } + }; + } + + protected getSelectedCell(): NotebookCellModel | undefined { + return this.notebookEditorWidgetService.focusedEditor?.model?.selectedCell; + } + + registerKeybindings(keybindings: KeybindingRegistry): void { + keybindings.registerKeybindings( + { + command: NotebookCellCommands.EDIT_COMMAND.id, + keybinding: 'Enter', + when: `!editorTextFocus && !inputFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`, + }, + { + command: NotebookCellCommands.STOP_EDIT_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.Alt, KeyModifier.CtrlCmd] }).toString(), + when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'markdown'`, + }, + { + command: NotebookCellCommands.STOP_EDIT_COMMAND.id, + keybinding: 'esc', + when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && !suggestWidgetVisible`, + }, + { + command: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.CtrlCmd] }).toString(), + when: `${NOTEBOOK_CELL_LIST_FOCUSED} && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`, + }, + { + command: NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_FOCUS_NEXT_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.Shift] }).toString(), + when: `${NOTEBOOK_CELL_LIST_FOCUSED} && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`, + }, + { + command: NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_INSERT_BELOW_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.Alt] }).toString(), + when: `${NOTEBOOK_CELL_LIST_FOCUSED} && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`, + }, + { + command: NotebookCellCommands.CLEAR_OUTPUTS_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.KEY_O, modifiers: [KeyModifier.Alt] }).toString(), + when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`, + }, + { + command: NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.KEY_P, modifiers: [KeyModifier.Alt] }).toString(), + when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`, + }, + { + command: NotebookCellCommands.TO_CODE_CELL_COMMAND.id, + keybinding: 'Y', + when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'markdown'`, + }, + { + command: NotebookCellCommands.TO_MARKDOWN_CELL_COMMAND.id, + keybinding: 'M', + when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`, + }, + { + command: NotebookCellCommands.SPLIT_CELL_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.MINUS, modifiers: [KeyModifier.CtrlCmd, KeyModifier.Shift] }).toString(), + when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`, + } + ); } } @@ -209,3 +572,4 @@ export namespace NotebookCellActionContribution { export const ADDITIONAL_OUTPUT_SIDEBAR_MENU = [...OUTPUT_SIDEBAR_MENU, 'more']; } + diff --git a/packages/notebook/src/browser/contributions/notebook-context-keys.ts b/packages/notebook/src/browser/contributions/notebook-context-keys.ts index 5bc987d154897..97c18175f327a 100644 --- a/packages/notebook/src/browser/contributions/notebook-context-keys.ts +++ b/packages/notebook/src/browser/contributions/notebook-context-keys.ts @@ -30,6 +30,7 @@ export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = 'notebookFindWidg export const NOTEBOOK_EDITOR_FOCUSED = 'notebookEditorFocused'; export const NOTEBOOK_CELL_LIST_FOCUSED = 'notebookCellListFocused'; export const NOTEBOOK_OUTPUT_FOCUSED = 'notebookOutputFocused'; +export const NOTEBOOK_OUTPUT_INPUT_FOCUSED = 'notebookOutputInputFocused'; export const NOTEBOOK_EDITOR_EDITABLE = 'notebookEditable'; export const NOTEBOOK_HAS_RUNNING_CELL = 'notebookHasRunningCell'; export const NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON = 'notebookUseConsolidatedOutputButton'; @@ -58,6 +59,9 @@ export const NOTEBOOK_INTERRUPTIBLE_KERNEL = 'notebookInterruptibleKernel'; export const NOTEBOOK_MISSING_KERNEL_EXTENSION = 'notebookMissingKernelExtension'; export const NOTEBOOK_HAS_OUTPUTS = 'notebookHasOutputs'; +export const NOTEBOOK_CELL_CURSOR_FIRST_LINE = 'cellEditorCursorPositionFirstLine'; +export const NOTEBOOK_CELL_CURSOR_LAST_LINE = 'cellEditorCursorPositionLastLine'; + export namespace NotebookContextKeys { export function initNotebookContextKeys(service: ContextKeyService): void { service.createKey(HAS_OPENED_NOTEBOOK, false); @@ -71,6 +75,7 @@ export namespace NotebookContextKeys { service.createKey(NOTEBOOK_EDITOR_FOCUSED, false); service.createKey(NOTEBOOK_CELL_LIST_FOCUSED, false); service.createKey(NOTEBOOK_OUTPUT_FOCUSED, false); + service.createKey(NOTEBOOK_OUTPUT_INPUT_FOCUSED, false); service.createKey(NOTEBOOK_EDITOR_EDITABLE, true); service.createKey(NOTEBOOK_HAS_RUNNING_CELL, false); service.createKey(NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, false); @@ -93,6 +98,8 @@ export namespace NotebookContextKeys { service.createKey(NOTEBOOK_CELL_INPUT_COLLAPSED, false); service.createKey(NOTEBOOK_CELL_OUTPUT_COLLAPSED, false); service.createKey(NOTEBOOK_CELL_RESOURCE, ''); + service.createKey(NOTEBOOK_CELL_CURSOR_FIRST_LINE, false); + service.createKey(NOTEBOOK_CELL_CURSOR_LAST_LINE, false); // Kernels service.createKey(NOTEBOOK_KERNEL, undefined); diff --git a/packages/notebook/src/browser/contributions/notebook-label-provider-contribution.ts b/packages/notebook/src/browser/contributions/notebook-label-provider-contribution.ts new file mode 100644 index 0000000000000..5e9be6fd13252 --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-label-provider-contribution.ts @@ -0,0 +1,85 @@ +// ***************************************************************************** +// 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 { codicon, LabelProvider, LabelProviderContribution } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { CellKind, CellUri } from '../../common'; +import { NotebookService } from '../service/notebook-service'; +import { NotebookCellOutlineNode } from './notebook-outline-contribution'; +import type Token = require('markdown-it/lib/token'); +import markdownit = require('@theia/core/shared/markdown-it'); +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { URI } from '@theia/core'; + +@injectable() +export class NotebookLabelProviderContribution implements LabelProviderContribution { + + @inject(NotebookService) + protected readonly notebookService: NotebookService; + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + protected markdownIt = markdownit(); + + canHandle(element: object): number { + if (NotebookCellOutlineNode.is(element)) { + return 200; + } + return 0; + } + + getIcon(element: NotebookCellOutlineNode): string { + const cell = this.findCellByUri(element.uri); + if (cell) { + return cell.cellKind === CellKind.Markup ? codicon('markdown') : codicon('code'); + } + return ''; + } + + getName(element: NotebookCellOutlineNode): string { + const cell = this.findCellByUri(element.uri); + if (cell) { + return cell.cellKind === CellKind.Code ? + cell.text.split('\n')[0] : + this.extractPlaintext(this.markdownIt.parse(cell.text.split('\n')[0], {})); + } + return ''; + } + + getLongName(element: NotebookCellOutlineNode): string { + const cell = this.findCellByUri(element.uri); + if (cell) { + return cell.cellKind === CellKind.Code ? + cell.text.split('\n')[0] : + this.extractPlaintext(this.markdownIt.parse(cell.text.split('\n')[0], {})); + } + return ''; + } + + extractPlaintext(parsedMarkdown: Token[]): string { + return parsedMarkdown.map(token => token.children ? this.extractPlaintext(token.children) : token.content).join(''); + } + + findCellByUri(uri: URI): NotebookCellModel | undefined { + const parsed = CellUri.parse(uri); + if (parsed) { + return this.notebookService.getNotebookEditorModel(parsed.notebook)?.cells.find(cell => cell.handle === parsed?.handle); + } + return undefined; + } + +} diff --git a/packages/notebook/src/browser/contributions/notebook-outline-contribution.ts b/packages/notebook/src/browser/contributions/notebook-outline-contribution.ts new file mode 100644 index 0000000000000..deea82d6a05b0 --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-outline-contribution.ts @@ -0,0 +1,114 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { codicon, FrontendApplicationContribution, LabelProvider, TreeNode } from '@theia/core/lib/browser'; +import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; +import { OutlineViewService } from '@theia/outline-view/lib/browser/outline-view-service'; +import { NotebookModel } from '../view-model/notebook-model'; +import { OutlineSymbolInformationNode } from '@theia/outline-view/lib/browser/outline-view-widget'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { DisposableCollection, isObject, URI } from '@theia/core'; +import { CellKind, CellUri } from '../../common'; +import { NotebookService } from '../service/notebook-service'; +export interface NotebookCellOutlineNode extends OutlineSymbolInformationNode { + uri: URI; +} + +export namespace NotebookCellOutlineNode { + export function is(element: object): element is NotebookCellOutlineNode { + return TreeNode.is(element) + && OutlineSymbolInformationNode.is(element) + && isObject(element) + && element.uri instanceof URI + && element.uri.scheme === CellUri.cellUriScheme; + } +} + +@injectable() +export class NotebookOutlineContribution implements FrontendApplicationContribution { + + @inject(NotebookEditorWidgetService) + protected readonly notebookEditorWidgetService: NotebookEditorWidgetService; + + @inject(OutlineViewService) + protected readonly outlineViewService: OutlineViewService; + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + @inject(NotebookService) + protected readonly notebookService: NotebookService; + + protected currentEditor?: NotebookEditorWidget; + + protected editorListeners: DisposableCollection = new DisposableCollection(); + protected editorModelListeners: DisposableCollection = new DisposableCollection(); + + onStart(): void { + this.notebookEditorWidgetService.onDidChangeFocusedEditor(editor => this.updateOutline(editor)); + + this.outlineViewService.onDidSelect(node => this.selectCell(node)); + this.outlineViewService.onDidTapNode(node => this.selectCell(node)); + } + + protected async updateOutline(editor: NotebookEditorWidget | undefined): Promise { + if (editor && !editor.isDisposed) { + await editor.ready; + this.currentEditor = editor; + this.editorListeners.dispose(); + this.editorListeners.push(editor.onDidChangeVisibility(() => { + if (this.currentEditor === editor && !editor.isVisible) { + this.outlineViewService.publish([]); + } + })); + if (editor.model) { + this.editorModelListeners.dispose(); + this.editorModelListeners.push(editor.model.onDidChangeSelectedCell(() => { + if (editor === this.currentEditor) { + this.updateOutline(editor); + } + })); + const roots = editor && editor.model && await this.createRoots(editor.model); + this.outlineViewService.publish(roots || []); + } + } + } + + protected async createRoots(model: NotebookModel): Promise { + return model.cells.map(cell => ({ + id: cell.uri.toString(), + iconClass: cell.cellKind === CellKind.Markup ? codicon('markdown') : codicon('code'), + parent: undefined, + children: [], + selected: model.selectedCell === cell, + expanded: false, + uri: cell.uri, + } as NotebookCellOutlineNode)); + } + + selectCell(node: object): void { + if (NotebookCellOutlineNode.is(node)) { + const parsed = CellUri.parse(node.uri); + const model = parsed && this.notebookService.getNotebookEditorModel(parsed.notebook); + const cell = model?.cells.find(c => c.handle === parsed?.handle); + if (model && cell) { + model.setSelectedCell(cell); + } + } + } + +} diff --git a/packages/notebook/src/browser/contributions/notebook-output-action-contribution.ts b/packages/notebook/src/browser/contributions/notebook-output-action-contribution.ts new file mode 100644 index 0000000000000..c7756fe507f43 --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-output-action-contribution.ts @@ -0,0 +1,82 @@ +// ***************************************************************************** +// 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 { Command, CommandContribution, CommandRegistry } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; +import { CellOutput, CellUri } from '../../common'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { EditorManager } from '@theia/editor/lib/browser'; + +export namespace NotebookOutputCommands { + export const ENABLE_SCROLLING = Command.toDefaultLocalizedCommand({ + id: 'cellOutput.enableScrolling', + }); + + export const OPEN_LARGE_OUTPUT = Command.toDefaultLocalizedCommand({ + id: 'workbench.action.openLargeOutput', + label: 'Open Large Output' + }); +} + +@injectable() +export class NotebookOutputActionContribution implements CommandContribution { + + @inject(NotebookEditorWidgetService) + protected readonly notebookEditorService: NotebookEditorWidgetService; + + @inject(EditorManager) + protected readonly editorManager: EditorManager; + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(NotebookOutputCommands.ENABLE_SCROLLING, { + execute: outputId => { + const [cell, output] = this.findOutputAndCell(outputId) ?? []; + if (cell && output?.metadata) { + output.metadata['scrollable'] = true; + cell.restartOutputRenderer(output.outputId); + } + } + }); + + commands.registerCommand(NotebookOutputCommands.OPEN_LARGE_OUTPUT, { + execute: outputId => { + const [cell, output] = this.findOutputAndCell(outputId) ?? []; + if (cell && output) { + this.editorManager.open(CellUri.generateCellOutputUri(CellUri.parse(cell.uri)!.notebook, output.outputId)); + } + } + }); + } + + protected findOutputAndCell(output: string): [NotebookCellModel, CellOutput] | undefined { + const model = this.notebookEditorService.focusedEditor?.model; + if (!model) { + return undefined; + } + + const outputId = output.slice(0, output.lastIndexOf('-')); + + for (const cell of model.cells) { + for (const outputModel of cell.outputs) { + if (outputModel.outputId === outputId) { + return [cell, outputModel]; + } + } + } + } + +} diff --git a/packages/notebook/src/browser/contributions/notebook-preferences.ts b/packages/notebook/src/browser/contributions/notebook-preferences.ts new file mode 100644 index 0000000000000..4187d6d914d80 --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-preferences.ts @@ -0,0 +1,92 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { nls } from '@theia/core'; +import { interfaces } from '@theia/core/shared/inversify'; +import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser'; + +export namespace NotebookPreferences { + export const NOTEBOOK_LINE_NUMBERS = 'notebook.lineNumbers'; + export const OUTPUT_LINE_HEIGHT = 'notebook.output.lineHeight'; + export const OUTPUT_FONT_SIZE = 'notebook.output.fontSize'; + export const OUTPUT_FONT_FAMILY = 'notebook.output.fontFamily'; + export const OUTPUT_SCROLLING = 'notebook.output.scrolling'; + export const OUTPUT_WORD_WRAP = 'notebook.output.wordWrap'; + export const OUTPUT_LINE_LIMIT = 'notebook.output.textLineLimit'; +} + +export const notebookPreferenceSchema: PreferenceSchema = { + properties: { + [NotebookPreferences.NOTEBOOK_LINE_NUMBERS]: { + type: 'string', + enum: ['on', 'off'], + default: 'off', + description: nls.localizeByDefault('Controls the display of line numbers in the cell editor.') + }, + [NotebookPreferences.OUTPUT_LINE_HEIGHT]: { + // eslint-disable-next-line max-len + markdownDescription: nls.localizeByDefault('Line height of the output text within notebook cells.\n - When set to 0, editor line height is used.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values.'), + type: 'number', + default: 0, + tags: ['notebookLayout', 'notebookOutputLayout'] + }, + [NotebookPreferences.OUTPUT_FONT_SIZE]: { + markdownDescription: nls.localizeByDefault('Font size for the output text within notebook cells. When set to 0, {0} is used.', '`#editor.fontSize#`'), + type: 'number', + default: 0, + tags: ['notebookLayout', 'notebookOutputLayout'] + }, + [NotebookPreferences.OUTPUT_FONT_FAMILY]: { + markdownDescription: nls.localizeByDefault('The font family of the output text within notebook cells. When set to empty, the {0} is used.', '`#editor.fontFamily#`'), + type: 'string', + tags: ['notebookLayout', 'notebookOutputLayout'] + }, + [NotebookPreferences.OUTPUT_SCROLLING]: { + markdownDescription: nls.localizeByDefault('Initially render notebook outputs in a scrollable region when longer than the limit.'), + type: 'boolean', + tags: ['notebookLayout', 'notebookOutputLayout'], + default: false + }, + [NotebookPreferences.OUTPUT_WORD_WRAP]: { + markdownDescription: nls.localizeByDefault('Controls whether the lines in output should wrap.'), + type: 'boolean', + tags: ['notebookLayout', 'notebookOutputLayout'], + default: false + }, + [NotebookPreferences.OUTPUT_LINE_LIMIT]: { + markdownDescription: nls.localizeByDefault( + 'Controls how many lines of text are displayed in a text output. If {0} is enabled, this setting is used to determine the scroll height of the output.', + '`#notebook.output.scrolling#`'), + type: 'number', + default: 30, + tags: ['notebookLayout', 'notebookOutputLayout'], + minimum: 1, + }, + + } +}; + +export const NotebookPreferenceContribution = Symbol('NotebookPreferenceContribution'); + +export function bindNotebookPreferences(bind: interfaces.Bind): void { + // We don't need a NotebookPreferenceConfiguration class, so there's no preference proxy to bind + bind(NotebookPreferenceContribution).toConstantValue({ schema: notebookPreferenceSchema }); + bind(PreferenceContribution).toService(NotebookPreferenceContribution); +} diff --git a/packages/notebook/src/browser/contributions/notebook-status-bar-contribution.ts b/packages/notebook/src/browser/contributions/notebook-status-bar-contribution.ts new file mode 100644 index 0000000000000..740970baf76ca --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-status-bar-contribution.ts @@ -0,0 +1,66 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; +import { StatusBar, StatusBarAlignment, Widget, WidgetStatusBarContribution } from '@theia/core/lib/browser'; +import { Disposable } from '@theia/core/lib/common'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { nls } from '@theia/core'; +import { NotebookCommands } from './notebook-actions-contribution'; + +export const NOTEBOOK_CELL_SELECTION_STATUS_BAR_ID = 'notebook-cell-selection-position'; + +@injectable() +export class NotebookStatusBarContribution implements WidgetStatusBarContribution { + + protected onDeactivate: Disposable | undefined; + + canHandle(widget: Widget): widget is NotebookEditorWidget { + return widget instanceof NotebookEditorWidget; + } + + activate(statusBar: StatusBar, widget: NotebookEditorWidget): void { + widget.ready.then(model => { + this.onDeactivate = model.onDidChangeSelectedCell(() => { + this.updateStatusbar(statusBar, widget); + }); + }); + this.updateStatusbar(statusBar, widget); + } + + deactivate(statusBar: StatusBar): void { + this.onDeactivate?.dispose(); + this.updateStatusbar(statusBar); + } + + protected async updateStatusbar(statusBar: StatusBar, editor?: NotebookEditorWidget): Promise { + const model = await editor?.ready; + if (!model || model.cells.length === 0 || !model.selectedCell) { + statusBar.removeElement(NOTEBOOK_CELL_SELECTION_STATUS_BAR_ID); + return; + } + + const selectedCellIndex = model.cells.indexOf(model.selectedCell) + 1; + + statusBar.setElement(NOTEBOOK_CELL_SELECTION_STATUS_BAR_ID, { + text: nls.localizeByDefault('Cell {0} of {1}', selectedCellIndex, model.cells.length), + alignment: StatusBarAlignment.RIGHT, + priority: 100, + command: NotebookCommands.CENTER_ACTIVE_CELL.id, + arguments: [editor] + }); + } +} diff --git a/packages/notebook/src/browser/contributions/notebook-undo-redo-handler.ts b/packages/notebook/src/browser/contributions/notebook-undo-redo-handler.ts new file mode 100644 index 0000000000000..367dae772664d --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-undo-redo-handler.ts @@ -0,0 +1,41 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { ApplicationShell, UndoRedoHandler } from '@theia/core/lib/browser'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; + +@injectable() +export class NotebookUndoRedoHandler implements UndoRedoHandler { + + @inject(ApplicationShell) + protected readonly applicationShell: ApplicationShell; + + priority = 200; + select(): NotebookEditorWidget | undefined { + const current = this.applicationShell.currentWidget; + if (current instanceof NotebookEditorWidget) { + return current; + } + return undefined; + } + undo(item: NotebookEditorWidget): void { + item.undo(); + } + redo(item: NotebookEditorWidget): void { + item.redo(); + } +} diff --git a/packages/notebook/src/browser/index.ts b/packages/notebook/src/browser/index.ts index eccec73435328..4fca99bd3a23c 100644 --- a/packages/notebook/src/browser/index.ts +++ b/packages/notebook/src/browser/index.ts @@ -23,4 +23,6 @@ export * from './service/notebook-kernel-service'; export * from './service/notebook-execution-state-service'; export * from './service/notebook-model-resolver-service'; export * from './service/notebook-renderer-messaging-service'; +export * from './service/notebook-cell-editor-service'; export * from './renderers/cell-output-webview'; +export * from './notebook-types'; diff --git a/packages/notebook/src/browser/notebook-cell-resource-resolver.ts b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts index 9e2db9b92b1c4..bfe3b3e606917 100644 --- a/packages/notebook/src/browser/notebook-cell-resource-resolver.ts +++ b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts @@ -14,20 +14,37 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Emitter, Resource, ResourceReadOptions, ResourceResolver, URI } from '@theia/core'; +import { Event, Emitter, Resource, ResourceReadOptions, ResourceResolver, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; import { CellUri } from '../common'; import { NotebookService } from './service/notebook-service'; import { NotebookCellModel } from './view-model/notebook-cell-model'; +import { NotebookModel } from './view-model/notebook-model'; export class NotebookCellResource implements Resource { - protected readonly didChangeContentsEmitter = new Emitter(); - readonly onDidChangeContents = this.didChangeContentsEmitter.event; + protected readonly onDidChangeContentsEmitter = new Emitter(); + get onDidChangeContents(): Event { + return this.onDidChangeContentsEmitter.event; + } + + get onDidChangeReadOnly(): Event | undefined { + return this.notebook.onDidChangeReadOnly; + } + + get readOnly(): boolean | MarkdownString | undefined { + return this.notebook.readOnly; + } + + protected cell: NotebookCellModel; + protected notebook: NotebookModel; - private cell: NotebookCellModel; + uri: URI; - constructor(public uri: URI, cell: NotebookCellModel) { + constructor(uri: URI, notebook: NotebookModel, cell: NotebookCellModel) { + this.uri = uri; + this.notebook = notebook; this.cell = cell; } @@ -36,7 +53,7 @@ export class NotebookCellResource implements Resource { } dispose(): void { - this.didChangeContentsEmitter.dispose(); + this.onDidChangeContentsEmitter.dispose(); } } @@ -48,7 +65,7 @@ export class NotebookCellResourceResolver implements ResourceResolver { protected readonly notebookService: NotebookService; async resolve(uri: URI): Promise { - if (uri.scheme !== CellUri.scheme) { + if (uri.scheme !== CellUri.cellUriScheme) { throw new Error(`Cannot resolve cell uri with scheme '${uri.scheme}'`); } @@ -69,7 +86,45 @@ export class NotebookCellResourceResolver implements ResourceResolver { throw new Error(`No cell found with handle '${parsedUri.handle}' in '${parsedUri.notebook}'`); } - return new NotebookCellResource(uri, notebookCellModel); + return new NotebookCellResource(uri, notebookModel, notebookCellModel); + } + +} + +@injectable() +export class NotebookOutputResourceResolver implements ResourceResolver { + + @inject(NotebookService) + protected readonly notebookService: NotebookService; + + async resolve(uri: URI): Promise { + if (uri.scheme !== CellUri.outputUriScheme) { + throw new Error(`Cannot resolve output uri with scheme '${uri.scheme}'`); + } + + const parsedUri = CellUri.parseCellOutputUri(uri); + if (!parsedUri) { + throw new Error(`Cannot parse uri '${uri.toString()}'`); + } + + const notebookModel = this.notebookService.getNotebookEditorModel(parsedUri.notebook); + + if (!notebookModel) { + throw new Error(`No notebook found for uri '${parsedUri.notebook}'`); + } + + const ouputModel = notebookModel.cells.flatMap(cell => cell.outputs).find(output => output.outputId === parsedUri.outputId); + + if (!ouputModel) { + throw new Error(`No output found with id '${parsedUri.outputId}' in '${parsedUri.notebook}'`); + } + + return { + uri: uri, + dispose: () => { }, + readContents: async () => ouputModel.outputs[0].data.toString(), + readOnly: true, + }; } } diff --git a/packages/notebook/src/browser/notebook-editor-widget-factory.ts b/packages/notebook/src/browser/notebook-editor-widget-factory.ts index fc9d90d0ac109..4cebc7c38663c 100644 --- a/packages/notebook/src/browser/notebook-editor-widget-factory.ts +++ b/packages/notebook/src/browser/notebook-editor-widget-factory.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { URI } from '@theia/core'; +import { nls, URI } from '@theia/core'; import { WidgetFactory, NavigatableWidgetOptions, LabelProvider } from '@theia/core/lib/browser'; import { inject, injectable } from '@theia/core/shared/inversify'; import { NotebookEditorWidget, NotebookEditorWidgetContainerFactory, NotebookEditorProps } from './notebook-editor-widget'; @@ -51,15 +51,17 @@ export class NotebookEditorWidgetFactory implements WidgetFactory { const editor = await this.createEditor(uri, options.notebookType); - const icon = this.labelProvider.getIcon(uri); - editor.title.label = this.labelProvider.getName(uri); - editor.title.iconClass = icon + ' file-icon'; - + this.setLabels(editor, uri); + const labelListener = this.labelProvider.onDidChange(event => { + if (event.affects(uri)) { + this.setLabels(editor, uri); + } + }); + editor.onDidDispose(() => labelListener.dispose()); return editor; } - private async createEditor(uri: URI, notebookType: string): Promise { - + protected async createEditor(uri: URI, notebookType: string): Promise { return this.createNotebookEditorWidget({ uri, notebookType, @@ -67,4 +69,14 @@ export class NotebookEditorWidgetFactory implements WidgetFactory { }); } + protected setLabels(editor: NotebookEditorWidget, uri: URI): void { + editor.title.caption = uri.path.fsPath(); + if (editor.model?.readOnly) { + editor.title.caption += ` • ${nls.localizeByDefault('Read-only')}`; + } + const icon = this.labelProvider.getIcon(uri); + editor.title.label = this.labelProvider.getName(uri); + editor.title.iconClass = icon + ' file-icon'; + } + } diff --git a/packages/notebook/src/browser/notebook-editor-widget.tsx b/packages/notebook/src/browser/notebook-editor-widget.tsx index 6ca2be986332c..1d8828e4048ed 100644 --- a/packages/notebook/src/browser/notebook-editor-widget.tsx +++ b/packages/notebook/src/browser/notebook-editor-widget.tsx @@ -16,9 +16,9 @@ import * as React from '@theia/core/shared/react'; import { CommandRegistry, MenuModelRegistry, URI } from '@theia/core'; -import { ReactWidget, Navigatable, SaveableSource, Message, DelegatingSaveable } from '@theia/core/lib/browser'; +import { ReactWidget, Navigatable, SaveableSource, Message, DelegatingSaveable, lock, unlock, animationFrame } from '@theia/core/lib/browser'; import { ReactNode } from '@theia/core/shared/react'; -import { CellKind } from '../common'; +import { CellKind, NotebookCellsChangeType } from '../common'; import { CellRenderer as CellRenderer, NotebookCellListView } from './view/notebook-cell-list-view'; import { NotebookCodeCellRenderer } from './view/notebook-code-cell-view'; import { NotebookMarkdownCellRenderer } from './view/notebook-markdown-cell-view'; @@ -28,6 +28,16 @@ import { inject, injectable, interfaces, postConstruct } from '@theia/core/share import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; import { NotebookEditorWidgetService } from './service/notebook-editor-widget-service'; import { NotebookMainToolbarRenderer } from './view/notebook-main-toolbar'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; +import { NotebookContextManager } from './service/notebook-context-manager'; +import { NotebookViewportService } from './view/notebook-viewport-service'; +import { NotebookCellCommands } from './contributions/notebook-cell-actions-contribution'; +import { NotebookFindWidget } from './view/notebook-find-widget'; +import debounce = require('lodash/debounce'); +import { CellOutputWebview, CellOutputWebviewFactory } from './renderers/cell-output-webview'; +import { NotebookCellOutputModel } from './view-model/notebook-cell-output-model'; +const PerfectScrollbar = require('react-perfect-scrollbar'); export const NotebookEditorWidgetContainerFactory = Symbol('NotebookEditorWidgetContainerFactory'); @@ -35,12 +45,28 @@ export function createNotebookEditorWidgetContainer(parent: interfaces.Container const child = parent.createChild(); child.bind(NotebookEditorProps).toConstantValue(props); + + const cellOutputWebviewFactory: CellOutputWebviewFactory = parent.get(CellOutputWebviewFactory); + child.bind(CellOutputWebview).toConstantValue(cellOutputWebviewFactory()); + + child.bind(NotebookContextManager).toSelf().inSingletonScope(); + child.bind(NotebookMainToolbarRenderer).toSelf().inSingletonScope(); + child.bind(NotebookCellToolbarFactory).toSelf().inSingletonScope(); + child.bind(NotebookCodeCellRenderer).toSelf().inSingletonScope(); + child.bind(NotebookMarkdownCellRenderer).toSelf().inSingletonScope(); + child.bind(NotebookViewportService).toSelf().inSingletonScope(); + child.bind(NotebookEditorWidget).toSelf(); return child; } -const NotebookEditorProps = Symbol('NotebookEditorProps'); +export const NotebookEditorProps = Symbol('NotebookEditorProps'); + +interface RenderMessage { + rendererId: string; + message: unknown; +} export interface NotebookEditorProps { uri: URI, @@ -70,6 +96,9 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa @inject(NotebookMainToolbarRenderer) protected notebookMainToolbarRenderer: NotebookMainToolbarRenderer; + @inject(NotebookContextManager) + protected notebookContextManager: NotebookContextManager; + @inject(NotebookCodeCellRenderer) protected codeCellRenderer: NotebookCodeCellRenderer; @inject(NotebookMarkdownCellRenderer) @@ -77,16 +106,55 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa @inject(NotebookEditorProps) protected readonly props: NotebookEditorProps; + @inject(NotebookViewportService) + protected readonly viewportService: NotebookViewportService; + + @inject(CellOutputWebview) + protected readonly cellOutputWebview: CellOutputWebview; + protected readonly onDidChangeModelEmitter = new Emitter(); readonly onDidChangeModel = this.onDidChangeModelEmitter.event; + protected readonly onDidChangeReadOnlyEmitter = new Emitter(); + readonly onDidChangeReadOnly = this.onDidChangeReadOnlyEmitter.event; + + protected readonly onPostKernelMessageEmitter = new Emitter(); + readonly onPostKernelMessage = this.onPostKernelMessageEmitter.event; + + protected readonly onDidPostKernelMessageEmitter = new Emitter(); + readonly onDidPostKernelMessage = this.onDidPostKernelMessageEmitter.event; + + protected readonly onPostRendererMessageEmitter = new Emitter(); + readonly onPostRendererMessage = this.onPostRendererMessageEmitter.event; + + protected readonly onDidReceiveKernelMessageEmitter = new Emitter(); + readonly onDidReceiveKernelMessage = this.onDidReceiveKernelMessageEmitter.event; + + protected readonly onDidChangeOutputInputFocusEmitter = new Emitter(); + readonly onDidChangeOutputInputFocus = this.onDidChangeOutputInputFocusEmitter.event; + protected readonly renderers = new Map(); protected _model?: NotebookModel; + protected _ready: Deferred = new Deferred(); + protected _findWidgetVisible = false; + protected _findWidgetRef = React.createRef(); + protected scrollBarRef = React.createRef<{ updateScroll(): void }>(); + protected debounceFind = debounce(() => { + this._findWidgetRef.current?.search({}); + }, 30, { + trailing: true, + maxWait: 100, + leading: false + }); get notebookType(): string { return this.props.notebookType; } + get ready(): Promise { + return this._ready.promise; + } + get model(): NotebookModel | undefined { return this._model; } @@ -94,31 +162,72 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa @postConstruct() protected init(): void { this.id = NOTEBOOK_EDITOR_ID_PREFIX + this.props.uri.toString(); - this.node.tabIndex = -1; + + this.scrollOptions = { + suppressScrollY: true + }; this.title.closable = true; this.update(); this.toDispose.push(this.onDidChangeModelEmitter); + this.toDispose.push(this.onDidChangeReadOnlyEmitter); this.renderers.set(CellKind.Markup, this.markdownCellRenderer); this.renderers.set(CellKind.Code, this.codeCellRenderer); - this.waitForData(); - + this._ready.resolve(this.waitForData()); + this.ready.then(model => { + if (model.cells.length === 1 && model.cells[0].source === '') { + this.commandRegistry.executeCommand(NotebookCellCommands.EDIT_COMMAND.id, model, model.cells[0]); + model.setSelectedCell(model.cells[0]); + } + model.onDidChangeContent(changeEvents => { + const cellEvent = changeEvents.filter(event => event.kind === NotebookCellsChangeType.Move || event.kind === NotebookCellsChangeType.ModelChange); + if (cellEvent.length > 0) { + this.cellOutputWebview.cellsChanged(cellEvent); + } + }); + }); } - protected async waitForData(): Promise { + protected async waitForData(): Promise { this._model = await this.props.notebookData; + this.cellOutputWebview.init(this._model, this); this.saveable.delegate = this._model; this.toDispose.push(this._model); + this.toDispose.push(this._model.onDidChangeContent(() => { + // Update the scroll bar content after the content has changed + // Wait one frame to ensure that the content has been rendered + animationFrame().then(() => this.scrollBarRef.current?.updateScroll()); + })); + this.toDispose.push(this._model.onContentChanged(() => { + if (this._findWidgetVisible) { + this.debounceFind(); + } + })); + this.toDispose.push(this._model.onDidChangeReadOnly(readOnly => { + if (readOnly) { + lock(this.title); + } else { + unlock(this.title); + } + this.onDidChangeReadOnlyEmitter.fire(readOnly); + this.update(); + })); + if (this._model.readOnly) { + lock(this.title); + } // Ensure that the model is loaded before adding the editor this.notebookEditorService.addNotebookEditor(this); + this._model.selectedCell = this._model.cells[0]; this.update(); + this.notebookContextManager.init(this); + return this._model; } protected override onActivateRequest(msg: Message): void { super.onActivateRequest(msg); - this.node.focus(); + (this.node.getElementsByClassName('theia-notebook-main-container')[0] as HTMLDivElement)?.focus(); } getResourceUri(): URI | undefined { @@ -126,37 +235,121 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa } createMoveToUri(resourceUri: URI): URI | undefined { - return this.props.uri; + return this.model?.uri.withPath(resourceUri.path); } undo(): void { - this.model?.undo(); + this._model?.undo(); } redo(): void { - this.model?.redo(); + this._model?.redo(); } protected render(): ReactNode { if (this._model) { - return
    - {this.notebookMainToolbarRenderer.render(this._model)} - + return
    +
    +
    + {this.notebookMainToolbarRenderer.render(this._model, this.node)} +
    this.viewportService.viewportElement = ref} + > + this.viewportService.onScroll(e)}> +
    + {this.cellOutputWebview.render()} + +
    +
    +
    ; } else { - return
    ; + return
    +
    +
    ; } } - protected override onAfterAttach(msg: Message): void { - super.onAfterAttach(msg); + protected override onCloseRequest(msg: Message): void { + super.onCloseRequest(msg); + this.notebookEditorService.removeNotebookEditor(this); } - protected override onAfterDetach(msg: Message): void { - super.onAfterDetach(msg); - this.notebookEditorService.removeNotebookEditor(this); + requestOuputPresentationChange(cellHandle: number, output?: NotebookCellOutputModel): void { + if (output) { + this.cellOutputWebview.requestOutputPresentationUpdate(cellHandle, output); + } + } + + postKernelMessage(message: unknown): void { + this.onDidPostKernelMessageEmitter.fire(message); + } + + postRendererMessage(rendererId: string, message: unknown): void { + this.onPostRendererMessageEmitter.fire({ rendererId, message }); + } + + recieveKernelMessage(message: unknown): void { + this.onDidReceiveKernelMessageEmitter.fire(message); + } + + outputInputFocusChanged(focused: boolean): void { + this.onDidChangeOutputInputFocusEmitter.fire(focused); + } + + showFindWidget(): void { + if (!this._findWidgetVisible) { + this._findWidgetVisible = true; + this.update(); + } + this._findWidgetRef.current?.focusSearch(this._model?.selectedText); + } + + override dispose(): void { + this.cellOutputWebview.dispose(); + this.notebookContextManager.dispose(); + this.onDidChangeModelEmitter.dispose(); + this.onDidPostKernelMessageEmitter.dispose(); + this.onDidReceiveKernelMessageEmitter.dispose(); + this.onPostRendererMessageEmitter.dispose(); + this.onDidChangeOutputInputFocusEmitter.dispose(); + this.viewportService.dispose(); + this._model?.dispose(); + super.dispose(); + } + + protected override onAfterShow(msg: Message): void { + super.onAfterShow(msg); + this.notebookEditorService.notebookEditorFocusChanged(this, true); + } + + protected override onAfterHide(msg: Message): void { + super.onAfterHide(msg); + this.notebookEditorService.notebookEditorFocusChanged(this, false); } } diff --git a/packages/notebook/src/browser/notebook-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index c3602f522216d..5671f2dc3355f 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -16,7 +16,9 @@ import '../../src/browser/style/index.css'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { OpenHandler, WidgetFactory } from '@theia/core/lib/browser'; +import { + FrontendApplicationContribution, KeybindingContribution, LabelProviderContribution, OpenHandler, UndoRedoHandler, WidgetFactory, WidgetStatusBarContribution +} from '@theia/core/lib/browser'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { NotebookOpenHandler } from './notebook-open-handler'; import { CommandContribution, MenuContribution, ResourceResolver, } from '@theia/core'; @@ -24,28 +26,34 @@ import { NotebookTypeRegistry } from './notebook-type-registry'; import { NotebookRendererRegistry } from './notebook-renderer-registry'; import { NotebookService } from './service/notebook-service'; import { NotebookEditorWidgetFactory } from './notebook-editor-widget-factory'; -import { NotebookCellResourceResolver } from './notebook-cell-resource-resolver'; +import { NotebookCellResourceResolver, NotebookOutputResourceResolver } from './notebook-cell-resource-resolver'; import { NotebookModelResolverService } from './service/notebook-model-resolver-service'; import { NotebookCellActionContribution } from './contributions/notebook-cell-actions-contribution'; -import { NotebookCellToolbarFactory } from './view/notebook-cell-toolbar-factory'; -import { createNotebookModelContainer, NotebookModel, NotebookModelFactory, NotebookModelProps } from './view-model/notebook-model'; +import { createNotebookModelContainer, NotebookModel, NotebookModelFactory, NotebookModelProps, NotebookModelResolverServiceProxy } from './view-model/notebook-model'; import { createNotebookCellModelContainer, NotebookCellModel, NotebookCellModelFactory, NotebookCellModelProps } from './view-model/notebook-cell-model'; import { createNotebookEditorWidgetContainer, NotebookEditorWidgetContainerFactory, NotebookEditorProps, NotebookEditorWidget } from './notebook-editor-widget'; -import { NotebookCodeCellRenderer } from './view/notebook-code-cell-view'; -import { NotebookMarkdownCellRenderer } from './view/notebook-markdown-cell-view'; import { NotebookActionsContribution } from './contributions/notebook-actions-contribution'; import { NotebookExecutionService } from './service/notebook-execution-service'; import { NotebookExecutionStateService } from './service/notebook-execution-state-service'; import { NotebookKernelService } from './service/notebook-kernel-service'; -import { KernelPickerMRUStrategy, NotebookKernelQuickPickService } from './service/notebook-kernel-quick-pick-service'; +import { NotebookKernelQuickPickService } from './service/notebook-kernel-quick-pick-service'; import { NotebookKernelHistoryService } from './service/notebook-kernel-history-service'; import { NotebookEditorWidgetService } from './service/notebook-editor-widget-service'; import { NotebookRendererMessagingService } from './service/notebook-renderer-messaging-service'; import { NotebookColorContribution } from './contributions/notebook-color-contribution'; -import { NotebookCellContextManager } from './service/notebook-cell-context-manager'; -import { NotebookMainToolbarRenderer } from './view/notebook-main-toolbar'; +import { NotebookMonacoTextModelService } from './service/notebook-monaco-text-model-service'; +import { NotebookOutlineContribution } from './contributions/notebook-outline-contribution'; +import { NotebookLabelProviderContribution } from './contributions/notebook-label-provider-contribution'; +import { NotebookOutputActionContribution } from './contributions/notebook-output-action-contribution'; +import { NotebookClipboardService } from './service/notebook-clipboard-service'; +import { bindNotebookPreferences } from './contributions/notebook-preferences'; +import { NotebookOptionsService } from './service/notebook-options'; +import { NotebookUndoRedoHandler } from './contributions/notebook-undo-redo-handler'; +import { NotebookStatusBarContribution } from './contributions/notebook-status-bar-contribution'; +import { NotebookCellEditorService } from './service/notebook-cell-editor-service'; +import { NotebookCellStatusBarService } from './service/notebook-cell-status-bar-service'; -export default new ContainerModule(bind => { +export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(NotebookColorContribution).toSelf().inSingletonScope(); bind(ColorContribution).toService(NotebookColorContribution); @@ -56,7 +64,6 @@ export default new ContainerModule(bind => { bind(NotebookRendererRegistry).toSelf().inSingletonScope(); bind(WidgetFactory).to(NotebookEditorWidgetFactory).inSingletonScope(); - bind(NotebookCellToolbarFactory).toSelf().inSingletonScope(); bind(NotebookService).toSelf().inSingletonScope(); bind(NotebookEditorWidgetService).toSelf().inSingletonScope(); @@ -65,23 +72,30 @@ export default new ContainerModule(bind => { bind(NotebookKernelService).toSelf().inSingletonScope(); bind(NotebookRendererMessagingService).toSelf().inSingletonScope(); bind(NotebookKernelHistoryService).toSelf().inSingletonScope(); - bind(NotebookKernelQuickPickService).to(KernelPickerMRUStrategy).inSingletonScope(); + bind(NotebookKernelQuickPickService).toSelf().inSingletonScope(); + bind(NotebookClipboardService).toSelf().inSingletonScope(); + bind(NotebookCellEditorService).toSelf().inSingletonScope(); + bind(NotebookCellStatusBarService).toSelf().inSingletonScope(); bind(NotebookCellResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(NotebookCellResourceResolver); bind(NotebookModelResolverService).toSelf().inSingletonScope(); + bind(NotebookModelResolverServiceProxy).toService(NotebookModelResolverService); + bind(NotebookOutputResourceResolver).toSelf().inSingletonScope(); + bind(ResourceResolver).toService(NotebookOutputResourceResolver); bind(NotebookCellActionContribution).toSelf().inSingletonScope(); bind(MenuContribution).toService(NotebookCellActionContribution); bind(CommandContribution).toService(NotebookCellActionContribution); + bind(KeybindingContribution).toService(NotebookCellActionContribution); bind(NotebookActionsContribution).toSelf().inSingletonScope(); bind(CommandContribution).toService(NotebookActionsContribution); bind(MenuContribution).toService(NotebookActionsContribution); + bind(KeybindingContribution).toService(NotebookActionsContribution); - bind(NotebookCodeCellRenderer).toSelf().inSingletonScope(); - bind(NotebookMarkdownCellRenderer).toSelf().inSingletonScope(); - bind(NotebookMainToolbarRenderer).toSelf().inSingletonScope(); + bind(NotebookOutputActionContribution).toSelf().inSingletonScope(); + bind(CommandContribution).toService(NotebookOutputActionContribution); bind(NotebookEditorWidgetContainerFactory).toFactory(ctx => (props: NotebookEditorProps) => createNotebookEditorWidgetContainer(ctx.container, props).get(NotebookEditorWidget) @@ -90,6 +104,22 @@ export default new ContainerModule(bind => { createNotebookModelContainer(ctx.container, props).get(NotebookModel) ); bind(NotebookCellModelFactory).toFactory(ctx => (props: NotebookCellModelProps) => - createNotebookCellModelContainer(ctx.container, props, NotebookCellContextManager).get(NotebookCellModel) + createNotebookCellModelContainer(ctx.container, props).get(NotebookCellModel) ); + + bind(NotebookMonacoTextModelService).toSelf().inSingletonScope(); + + bind(NotebookOutlineContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(NotebookOutlineContribution); + bind(NotebookLabelProviderContribution).toSelf().inSingletonScope(); + bind(LabelProviderContribution).toService(NotebookLabelProviderContribution); + + bindNotebookPreferences(bind); + bind(NotebookOptionsService).toSelf().inSingletonScope(); + + bind(NotebookUndoRedoHandler).toSelf().inSingletonScope(); + bind(UndoRedoHandler).toService(NotebookUndoRedoHandler); + + bind(NotebookStatusBarContribution).toSelf().inSingletonScope(); + bind(WidgetStatusBarContribution).toService(NotebookStatusBarContribution); }); diff --git a/packages/notebook/src/browser/notebook-open-handler.ts b/packages/notebook/src/browser/notebook-open-handler.ts index b260e24c13ac3..80181f7a1a6fd 100644 --- a/packages/notebook/src/browser/notebook-open-handler.ts +++ b/packages/notebook/src/browser/notebook-open-handler.ts @@ -14,32 +14,60 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { URI, MaybePromise } from '@theia/core'; -import { NavigatableWidgetOpenHandler, WidgetOpenerOptions } from '@theia/core/lib/browser'; +import { URI, MaybePromise, Disposable } from '@theia/core'; +import { NavigatableWidgetOpenHandler, PreferenceService, WidgetOpenerOptions, getDefaultHandler, defaultHandlerPriority } from '@theia/core/lib/browser'; import { inject, injectable } from '@theia/core/shared/inversify'; import { NotebookFileSelector, NotebookTypeDescriptor } from '../common/notebook-protocol'; -import { NotebookTypeRegistry } from './notebook-type-registry'; import { NotebookEditorWidget } from './notebook-editor-widget'; import { match } from '@theia/core/lib/common/glob'; import { NotebookEditorWidgetOptions } from './notebook-editor-widget-factory'; +export interface NotebookWidgetOpenerOptions extends WidgetOpenerOptions { + notebookType?: string; +} + @injectable() export class NotebookOpenHandler extends NavigatableWidgetOpenHandler { - id: string = 'notebook'; + readonly id = NotebookEditorWidget.ID; + + protected notebookTypes: NotebookTypeDescriptor[] = []; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + registerNotebookType(notebookType: NotebookTypeDescriptor): Disposable { + this.notebookTypes.push(notebookType); + return Disposable.create(() => { + this.notebookTypes.splice(this.notebookTypes.indexOf(notebookType), 1); + }); + } - @inject(NotebookTypeRegistry) - protected notebookTypeRegistry: NotebookTypeRegistry; + canHandle(uri: URI, options?: NotebookWidgetOpenerOptions): MaybePromise { + const defaultHandler = getDefaultHandler(uri, this.preferenceService); + if (options?.notebookType) { + return this.canHandleType(uri, this.notebookTypes.find(type => type.type === options.notebookType), defaultHandler); + } + return Math.max(...this.notebookTypes.map(type => this.canHandleType(uri, type), defaultHandler)); + } - canHandle(uri: URI, options?: WidgetOpenerOptions | undefined): MaybePromise { - const priorities = this.notebookTypeRegistry.notebookTypes - .filter(notebook => notebook.selector && this.matches(notebook.selector, uri)) - .map(notebook => this.calculatePriority(notebook)); - return Math.max(...priorities); + canHandleType(uri: URI, notebookType?: NotebookTypeDescriptor, defaultHandler?: string): number { + if (notebookType?.selector && this.matches(notebookType.selector, uri)) { + return notebookType.type === defaultHandler ? defaultHandlerPriority : this.calculatePriority(notebookType); + } else { + return 0; + } + } + + protected calculatePriority(notebookType: NotebookTypeDescriptor | undefined): number { + if (!notebookType) { + return 0; + } + return notebookType.priority === 'option' ? 100 : 200; } protected findHighestPriorityType(uri: URI): NotebookTypeDescriptor | undefined { - const matchingTypes = this.notebookTypeRegistry.notebookTypes + const matchingTypes = this.notebookTypes .filter(notebookType => notebookType.selector && this.matches(notebookType.selector, uri)) .map(notebookType => ({ descriptor: notebookType, priority: this.calculatePriority(notebookType) })); @@ -56,16 +84,22 @@ export class NotebookOpenHandler extends NavigatableWidgetOpenHandler { + return super.open(uri, options); } - protected override createWidgetOptions(uri: URI, options?: WidgetOpenerOptions | undefined): NotebookEditorWidgetOptions { + protected override createWidgetOptions(uri: URI, options?: NotebookWidgetOpenerOptions): NotebookEditorWidgetOptions { const widgetOptions = super.createWidgetOptions(uri, options); - const notebookType = this.findHighestPriorityType(uri); + if (options?.notebookType) { + return { + notebookType: options.notebookType, + ...widgetOptions + }; + } + const defaultHandler = getDefaultHandler(uri, this.preferenceService); + const notebookType = this.notebookTypes.find(type => type.type === defaultHandler) + || this.findHighestPriorityType(uri); if (!notebookType) { throw new Error('No notebook types registered for uri: ' + uri.toString()); } @@ -75,11 +109,11 @@ export class NotebookOpenHandler extends NavigatableWidgetOpenHandler this.selectorMatches(selector, resource)); } - selectorMatches(selector: NotebookFileSelector, resource: URI): boolean { + protected selectorMatches(selector: NotebookFileSelector, resource: URI): boolean { return !!selector.filenamePattern && match(selector.filenamePattern, resource.path.name + resource.path.ext); } diff --git a/packages/notebook/src/browser/notebook-output-utils.ts b/packages/notebook/src/browser/notebook-output-utils.ts new file mode 100644 index 0000000000000..1ef5082502248 --- /dev/null +++ b/packages/notebook/src/browser/notebook-output-utils.ts @@ -0,0 +1,119 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * Copied from commit 18b2c92451b076943e5b508380e0eba66ba7d934 from file src\vs\workbench\contrib\notebook\common\notebookCommon.ts + *--------------------------------------------------------------------------------------------*/ + +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; + +const textDecoder = new TextDecoder(); + +/** + * Given a stream of individual stdout outputs, this function will return the compressed lines, escaping some of the common terminal escape codes. + * E.g. some terminal escape codes would result in the previous line getting cleared, such if we had 3 lines and + * last line contained such a code, then the result string would be just the first two lines. + * @returns a single VSBuffer with the concatenated and compressed data, and whether any compression was done. + */ +export function compressOutputItemStreams(outputs: Uint8Array[]): { data: BinaryBuffer, didCompression: boolean } { + const buffers: Uint8Array[] = []; + let startAppending = false; + + // Pick the first set of outputs with the same mime type. + for (const output of outputs) { + if ((buffers.length === 0 || startAppending)) { + buffers.push(output); + startAppending = true; + } + } + + let didCompression = compressStreamBuffer(buffers); + const concatenated = BinaryBuffer.concat(buffers.map(buffer => BinaryBuffer.wrap(buffer))); + const data = formatStreamText(concatenated); + didCompression = didCompression || data.byteLength !== concatenated.byteLength; + return { data, didCompression }; +} + +export const MOVE_CURSOR_1_LINE_COMMAND = `${String.fromCharCode(27)}[A`; +const MOVE_CURSOR_1_LINE_COMMAND_BYTES = MOVE_CURSOR_1_LINE_COMMAND.split('').map(c => c.charCodeAt(0)); +const LINE_FEED = 10; +function compressStreamBuffer(streams: Uint8Array[]): boolean { + let didCompress = false; + streams.forEach((stream, index) => { + if (index === 0 || stream.length < MOVE_CURSOR_1_LINE_COMMAND.length) { + return; + } + + const previousStream = streams[index - 1]; + + // Remove the previous line if required. + const command = stream.subarray(0, MOVE_CURSOR_1_LINE_COMMAND.length); + if (command[0] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[0] && command[1] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[1] && command[2] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[2]) { + const lastIndexOfLineFeed = previousStream.lastIndexOf(LINE_FEED); + if (lastIndexOfLineFeed === -1) { + return; + } + + didCompress = true; + streams[index - 1] = previousStream.subarray(0, lastIndexOfLineFeed); + streams[index] = stream.subarray(MOVE_CURSOR_1_LINE_COMMAND.length); + } + }); + return didCompress; +} + +const BACKSPACE_CHARACTER = '\b'.charCodeAt(0); +const CARRIAGE_RETURN_CHARACTER = '\r'.charCodeAt(0); +function formatStreamText(buffer: BinaryBuffer): BinaryBuffer { + // We have special handling for backspace and carriage return characters. + // Don't unnecessary decode the bytes if we don't need to perform any processing. + if (!buffer.buffer.includes(BACKSPACE_CHARACTER) && !buffer.buffer.includes(CARRIAGE_RETURN_CHARACTER)) { + return buffer; + } + // Do the same thing jupyter is doing + return BinaryBuffer.fromString(fixCarriageReturn(fixBackspace(textDecoder.decode(buffer.buffer)))); +} + +/** + * Took this from jupyter/notebook + * https://github.com/jupyter/notebook/blob/b8b66332e2023e83d2ee04f83d8814f567e01a4e/notebook/static/base/js/utils.js + * Remove characters that are overridden by backspace characters + */ +function fixBackspace(txt: string): string { + let tmp = txt; + do { + txt = tmp; + // Cancel out anything-but-newline followed by backspace + tmp = txt.replace(/[^\n]\x08/gm, ''); + } while (tmp.length < txt.length); + return txt; +} + +/** + * Remove chunks that should be overridden by the effect of carriage return characters + * From https://github.com/jupyter/notebook/blob/master/notebook/static/base/js/utils.js + */ +function fixCarriageReturn(txt: string): string { + txt = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline + while (txt.search(/\r[^$]/g) > -1) { + const base = txt.match(/^(.*)\r+/m)![1]; + let insert = txt.match(/\r+(.*)$/m)![1]; + insert = insert + base.slice(insert.length, base.length); + txt = txt.replace(/\r+.*$/m, '\r').replace(/^.*\r/m, insert); + } + return txt; +} diff --git a/packages/notebook/src/browser/notebook-renderer-registry.ts b/packages/notebook/src/browser/notebook-renderer-registry.ts index cad1d9c6ba772..4a945c447dce6 100644 --- a/packages/notebook/src/browser/notebook-renderer-registry.ts +++ b/packages/notebook/src/browser/notebook-renderer-registry.ts @@ -30,6 +30,11 @@ export interface NotebookRendererInfo { readonly requiresMessaging: boolean; } +export interface NotebookPreloadInfo { + readonly type: string; + readonly entrypoint: string; +} + @injectable() export class NotebookRendererRegistry { @@ -39,6 +44,12 @@ export class NotebookRendererRegistry { return this._notebookRenderers; } + private readonly _staticNotebookPreloads: NotebookPreloadInfo[] = []; + + get staticNotebookPreloads(): readonly NotebookPreloadInfo[] { + return this._staticNotebookPreloads; + } + registerNotebookRenderer(type: NotebookRendererDescriptor, basePath: string): Disposable { let entrypoint; if (typeof type.entrypoint === 'string') { @@ -62,5 +73,13 @@ export class NotebookRendererRegistry { this._notebookRenderers.splice(this._notebookRenderers.findIndex(renderer => renderer.id === type.id), 1); }); } + + registerStaticNotebookPreload(type: string, entrypoint: string, basePath: string): Disposable { + const staticPreload = { type, entrypoint: new Path(basePath).join(entrypoint).toString() }; + this._staticNotebookPreloads.push(staticPreload); + return Disposable.create(() => { + this._staticNotebookPreloads.splice(this._staticNotebookPreloads.indexOf(staticPreload), 1); + }); + } } diff --git a/packages/notebook/src/browser/notebook-type-registry.ts b/packages/notebook/src/browser/notebook-type-registry.ts index 923d982c22cb9..1911e5bb9b3b4 100644 --- a/packages/notebook/src/browser/notebook-type-registry.ts +++ b/packages/notebook/src/browser/notebook-type-registry.ts @@ -13,22 +13,42 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Disposable } from '@theia/core'; -import { injectable } from '@theia/core/shared/inversify'; + +import { Disposable, DisposableCollection } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { OpenWithService } from '@theia/core/lib/browser'; import { NotebookTypeDescriptor } from '../common/notebook-protocol'; +import { NotebookOpenHandler } from './notebook-open-handler'; @injectable() export class NotebookTypeRegistry { + + @inject(OpenWithService) + protected readonly openWithService: OpenWithService; + + @inject(NotebookOpenHandler) + protected readonly notebookOpenHandler: NotebookOpenHandler; + private readonly _notebookTypes: NotebookTypeDescriptor[] = []; get notebookTypes(): readonly NotebookTypeDescriptor[] { return this._notebookTypes; } - registerNotebookType(type: NotebookTypeDescriptor): Disposable { - this._notebookTypes.push(type); - return Disposable.create(() => { + registerNotebookType(type: NotebookTypeDescriptor, providerName: string): Disposable { + const toDispose = new DisposableCollection(); + toDispose.push(Disposable.create(() => { this._notebookTypes.splice(this._notebookTypes.indexOf(type), 1); - }); + })); + this._notebookTypes.push(type); + toDispose.push(this.notebookOpenHandler.registerNotebookType(type)); + toDispose.push(this.openWithService.registerHandler({ + id: type.type, + label: type.displayName, + providerName, + canHandle: uri => this.notebookOpenHandler.canHandleType(uri, type), + open: uri => this.notebookOpenHandler.open(uri, { notebookType: type.type }) + })); + return toDispose; } } diff --git a/packages/notebook/src/browser/notebook-types.ts b/packages/notebook/src/browser/notebook-types.ts new file mode 100644 index 0000000000000..f1bb70b5eab11 --- /dev/null +++ b/packages/notebook/src/browser/notebook-types.ts @@ -0,0 +1,186 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { + CellData, CellEditType, CellMetadataEdit, CellOutput, CellOutputItem, CellRange, NotebookCellContentChangeEvent, + NotebookCellInternalMetadata, + NotebookCellMetadata, + NotebookCellsChangeInternalMetadataEvent, + NotebookCellsChangeLanguageEvent, + NotebookCellsChangeMetadataEvent, + NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookDocumentMetadata +} from '../common'; +import { NotebookCell } from './view-model/notebook-cell-model'; + +export interface NotebookTextModelChangedEvent { + readonly rawEvents: NotebookContentChangedEvent[]; + // readonly versionId: number; + readonly synchronous?: boolean; + readonly endSelectionState?: SelectionState; +}; + +export type NotebookContentChangedEvent = (NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | + NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookOutputItemChangedEvent | + NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent | + NotebookCellsChangeInternalMetadataEvent | NotebookDocumentUnknownChangeEvent); // & { transient: boolean }; + +export interface NotebookCellsInitializeEvent { + readonly kind: NotebookCellsChangeType.Initialize; + readonly changes: NotebookCellTextModelSplice[]; +} + +export interface NotebookDocumentChangeMetadataEvent { + readonly kind: NotebookCellsChangeType.ChangeDocumentMetadata; + readonly metadata: NotebookDocumentMetadata; +} + +export interface NotebookCellsModelChangedEvent { + readonly kind: NotebookCellsChangeType.ModelChange; + readonly changes: NotebookCellTextModelSplice[]; +} + +export interface NotebookModelWillAddRemoveEvent { + readonly rawEvent: NotebookCellsModelChangedEvent; +}; + +export interface NotebookCellsModelMoveEvent { + readonly kind: NotebookCellsChangeType.Move; + readonly index: number; + readonly length: number; + readonly newIdx: number; + readonly cells: T[]; +} + +export interface NotebookOutputChangedEvent { + readonly kind: NotebookCellsChangeType.Output; + readonly index: number; + readonly outputs: CellOutput[]; + readonly append: boolean; +} + +export interface NotebookOutputItemChangedEvent { + readonly kind: NotebookCellsChangeType.OutputItem; + readonly index: number; + readonly outputId: string; + readonly outputItems: CellOutputItem[]; + readonly append: boolean; +} + +export interface NotebookDocumentUnknownChangeEvent { + readonly kind: NotebookCellsChangeType.Unknown; +} + +export enum SelectionStateType { + Handle = 0, + Index = 1 +} + +export interface SelectionHandleState { + kind: SelectionStateType.Handle; + primary: number | null; + selections: number[]; +} + +export interface SelectionIndexState { + kind: SelectionStateType.Index; + focus: CellRange; + selections: CellRange[]; +} + +export type SelectionState = SelectionHandleState | SelectionIndexState; + +export interface NotebookModelWillAddRemoveEvent { + readonly newCellIds?: number[]; + readonly rawEvent: NotebookCellsModelChangedEvent; +}; + +export interface CellOutputEdit { + editType: CellEditType.Output; + index: number; + outputs: CellOutput[]; + deleteCount?: number; + append?: boolean; +} + +export interface CellOutputEditByHandle { + editType: CellEditType.Output; + handle: number; + outputs: CellOutput[]; + deleteCount?: number; + append?: boolean; +} + +export interface CellOutputItemEdit { + editType: CellEditType.OutputItems; + items: CellOutputItem[]; + outputId: string; + append?: boolean; +} + +export interface CellLanguageEdit { + editType: CellEditType.CellLanguage; + index: number; + language: string; +} + +export interface DocumentMetadataEdit { + editType: CellEditType.DocumentMetadata; + metadata: NotebookDocumentMetadata; +} + +export interface CellMoveEdit { + editType: CellEditType.Move; + index: number; + length: number; + newIdx: number; +} + +export interface CellReplaceEdit { + editType: CellEditType.Replace; + index: number; + count: number; + cells: CellData[]; +} + +export interface CellPartialMetadataEdit { + editType: CellEditType.PartialMetadata; + index: number; + metadata: NullablePartialNotebookCellMetadata; +} + +export type ImmediateCellEditOperation = CellOutputEditByHandle | CellOutputItemEdit | CellPartialInternalMetadataEditByHandle; // add more later on +export type CellEditOperation = ImmediateCellEditOperation | CellReplaceEdit | CellOutputEdit | + CellMetadataEdit | CellLanguageEdit | DocumentMetadataEdit | CellMoveEdit | CellPartialMetadataEdit; // add more later on + +export type NullablePartialNotebookCellInternalMetadata = { + [Key in keyof Partial]: NotebookCellInternalMetadata[Key] | null +}; + +export type NullablePartialNotebookCellMetadata = { + [Key in keyof Partial]: NotebookCellMetadata[Key] | null +}; + +export interface CellPartialInternalMetadataEditByHandle { + editType: CellEditType.PartialInternalMetadata; + handle: number; + internalMetadata: NullablePartialNotebookCellInternalMetadata; +} + +export interface NotebookCellOutputsSplice { + start: number; + deleteCount: number; + newOutputs: CellOutput[]; +}; diff --git a/packages/notebook/src/browser/renderers/cell-output-webview.ts b/packages/notebook/src/browser/renderers/cell-output-webview.ts index 0466ef6b6c9e2..e838f143df911 100644 --- a/packages/notebook/src/browser/renderers/cell-output-webview.ts +++ b/packages/notebook/src/browser/renderers/cell-output-webview.ts @@ -14,19 +14,38 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Disposable } from '@theia/core'; +import { Disposable, Event } from '@theia/core'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { NotebookContentChangedEvent } from '../notebook-types'; +import { NotebookCellOutputModel } from '../view-model/notebook-cell-output-model'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; export const CellOutputWebviewFactory = Symbol('outputWebviewFactory'); +export const CellOutputWebview = Symbol('outputWebview'); -export type CellOutputWebviewFactory = (cell: NotebookCellModel) => Promise; +export type CellOutputWebviewFactory = () => Promise; + +export interface OutputRenderEvent { + cellHandle: number; + outputId: string; + outputHeight: number; +} export interface CellOutputWebview extends Disposable { readonly id: string; + init(notebook: NotebookModel, editor: NotebookEditorWidget): void; + render(): React.ReactNode; + setCellHeight(cell: NotebookCellModel, height: number): void; + cellsChanged(cellEvent: NotebookContentChangedEvent[]): void; + onDidRenderOutput: Event + + requestOutputPresentationUpdate(cellHandle: number, output: NotebookCellOutputModel): void; + attachWebview(): void; isAttached(): boolean } diff --git a/packages/notebook/src/browser/service/notebook-cell-context-manager.ts b/packages/notebook/src/browser/service/notebook-cell-context-manager.ts deleted file mode 100644 index aefe1655a4ad6..0000000000000 --- a/packages/notebook/src/browser/service/notebook-cell-context-manager.ts +++ /dev/null @@ -1,68 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2023 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 { inject, injectable } from '@theia/core/shared/inversify'; -import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; -import { NotebookCellModel } from '../view-model/notebook-cell-model'; -import { NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE } from '../contributions/notebook-context-keys'; -import { Disposable, DisposableCollection, Emitter } from '@theia/core'; -import { CellKind } from '../../common'; -import { NotebookExecutionStateService } from '../service/notebook-execution-state-service'; - -@injectable() -export class NotebookCellContextManager implements Disposable { - @inject(ContextKeyService) protected contextKeyService: ContextKeyService; - - @inject(NotebookExecutionStateService) - protected readonly executionStateService: NotebookExecutionStateService; - - protected readonly toDispose = new DisposableCollection(); - - protected currentContext: HTMLLIElement; - - protected readonly onDidChangeContextEmitter = new Emitter(); - readonly onDidChangeContext = this.onDidChangeContextEmitter.event; - - updateCellContext(cell: NotebookCellModel, newHtmlContext: HTMLLIElement): void { - if (newHtmlContext !== this.currentContext) { - this.toDispose.dispose(); - - this.currentContext = newHtmlContext; - const currentStore = this.contextKeyService.createScoped(newHtmlContext); - this.toDispose.push(currentStore); - - currentStore.setContext(NOTEBOOK_CELL_TYPE, cell.cellKind === CellKind.Code ? 'code' : 'markdown'); - - this.toDispose.push(cell.onDidRequestCellEditChange(cellEdit => { - currentStore?.setContext(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, cellEdit); - this.onDidChangeContextEmitter.fire(); - })); - this.toDispose.push(this.executionStateService.onDidChangeExecution(e => { - if (e.affectsCell(cell.uri)) { - currentStore?.setContext(NOTEBOOK_CELL_EXECUTING, !!e.changed); - currentStore?.setContext(NOTEBOOK_CELL_EXECUTION_STATE, e.changed?.state ?? 'idle'); - this.onDidChangeContextEmitter.fire(); - } - })); - this.onDidChangeContextEmitter.fire(); - } - } - - dispose(): void { - this.toDispose.dispose(); - this.onDidChangeContextEmitter.dispose(); - } -} diff --git a/packages/notebook/src/browser/service/notebook-cell-editor-service.ts b/packages/notebook/src/browser/service/notebook-cell-editor-service.ts new file mode 100644 index 0000000000000..53268eb62b4ec --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-cell-editor-service.ts @@ -0,0 +1,74 @@ +// ***************************************************************************** +// 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 { Emitter, URI } from '@theia/core'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor'; +import { NotebookEditorWidgetService } from './notebook-editor-widget-service'; +import { CellUri } from '../../common'; + +@injectable() +export class NotebookCellEditorService { + + @inject(NotebookEditorWidgetService) + protected readonly notebookEditorWidgetService: NotebookEditorWidgetService; + + protected onDidChangeCellEditorsEmitter = new Emitter(); + readonly onDidChangeCellEditors = this.onDidChangeCellEditorsEmitter.event; + + protected onDidChangeFocusedCellEditorEmitter = new Emitter(); + readonly onDidChangeFocusedCellEditor = this.onDidChangeFocusedCellEditorEmitter.event; + + protected currentActiveCell?: SimpleMonacoEditor; + + protected currentCellEditors: Map = new Map(); + + @postConstruct() + protected init(): void { + this.notebookEditorWidgetService.onDidChangeCurrentEditor(editor => { + // if defocus notebook editor or another notebook editor is focused, clear the active cell + if (!editor || (this.currentActiveCell && CellUri.parse(this.currentActiveCell.uri)?.notebook.toString() !== editor?.model?.uri.toString())) { + this.currentActiveCell = undefined; + this.onDidChangeFocusedCellEditorEmitter.fire(undefined); + } + }); + } + + get allCellEditors(): SimpleMonacoEditor[] { + return Array.from(this.currentCellEditors.values()); + } + + editorCreated(uri: URI, editor: SimpleMonacoEditor): void { + this.currentCellEditors.set(uri.toString(), editor); + this.onDidChangeCellEditorsEmitter.fire(); + } + + editorDisposed(uri: URI): void { + this.currentCellEditors.delete(uri.toString()); + this.onDidChangeCellEditorsEmitter.fire(); + } + + editorFocusChanged(editor?: SimpleMonacoEditor): void { + if (editor) { + this.currentActiveCell = editor; + this.onDidChangeFocusedCellEditorEmitter.fire(editor); + } + } + + getActiveCell(): SimpleMonacoEditor | undefined { + return this.currentActiveCell; + } +} diff --git a/packages/notebook/src/browser/service/notebook-cell-status-bar-service.ts b/packages/notebook/src/browser/service/notebook-cell-status-bar-service.ts new file mode 100644 index 0000000000000..92fedf6014325 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-cell-status-bar-service.ts @@ -0,0 +1,94 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, Command, Disposable, Emitter, Event, URI } from '@theia/core'; +import { CellStatusbarAlignment } from '../../common'; +import { ThemeColor } from '@theia/core/lib/common/theme'; +import { AccessibilityInformation } from '@theia/core/lib/common/accessibility'; +import { injectable } from '@theia/core/shared/inversify'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; + +export interface NotebookCellStatusBarItem { + readonly alignment: CellStatusbarAlignment; + readonly priority?: number; + readonly text: string; + readonly color?: string | ThemeColor; + readonly backgroundColor?: string | ThemeColor; + readonly tooltip?: string | MarkdownString; + readonly command?: string | (Command & { arguments?: unknown[] }); + readonly accessibilityInformation?: AccessibilityInformation; + readonly opacity?: string; + readonly onlyShowWhenActive?: boolean; +} +export interface NotebookCellStatusBarItemList { + items: NotebookCellStatusBarItem[]; + dispose?(): void; +} + +export interface NotebookCellStatusBarItemProvider { + viewType: string; + onDidChangeStatusBarItems?: Event; + provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise; +} + +@injectable() +export class NotebookCellStatusBarService implements Disposable { + + protected readonly onDidChangeProvidersEmitter = new Emitter(); + readonly onDidChangeProviders: Event = this.onDidChangeProvidersEmitter.event; + + protected readonly onDidChangeItemsEmitter = new Emitter(); + readonly onDidChangeItems: Event = this.onDidChangeItemsEmitter.event; + + protected readonly providers: NotebookCellStatusBarItemProvider[] = []; + + registerCellStatusBarItemProvider(provider: NotebookCellStatusBarItemProvider): Disposable { + this.providers.push(provider); + let changeListener: Disposable | undefined; + if (provider.onDidChangeStatusBarItems) { + changeListener = provider.onDidChangeStatusBarItems(() => this.onDidChangeItemsEmitter.fire()); + } + + this.onDidChangeProvidersEmitter.fire(); + + return Disposable.create(() => { + changeListener?.dispose(); + const idx = this.providers.findIndex(p => p === provider); + this.providers.splice(idx, 1); + }); + } + + async getStatusBarItemsForCell(notebookUri: URI, cellIndex: number, viewType: string, token: CancellationToken): Promise { + const providers = this.providers.filter(p => p.viewType === viewType || p.viewType === '*'); + return Promise.all(providers.map(async p => { + try { + return await p.provideCellStatusBarItems(notebookUri, cellIndex, token) ?? { items: [] }; + } catch (e) { + console.error(e); + return { items: [] }; + } + })); + } + + dispose(): void { + this.onDidChangeItemsEmitter.dispose(); + this.onDidChangeProvidersEmitter.dispose(); + } +} diff --git a/packages/notebook/src/browser/service/notebook-clipboard-service.ts b/packages/notebook/src/browser/service/notebook-clipboard-service.ts new file mode 100644 index 0000000000000..8698fe518cec8 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-clipboard-service.ts @@ -0,0 +1,43 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { environment } from '@theia/core'; +import { CellData } from '../../common'; + +@injectable() +export class NotebookClipboardService { + + protected copiedCell: CellData | undefined; + + @inject(ClipboardService) + protected readonly clipboardService: ClipboardService; + + copyCell(cell: NotebookCellModel): void { + this.copiedCell = cell.getData(); + + if (environment.electron.is()) { + this.clipboardService.writeText(cell.text); + } + } + + getCell(): CellData | undefined { + return this.copiedCell; + } + +} diff --git a/packages/notebook/src/browser/service/notebook-context-manager.ts b/packages/notebook/src/browser/service/notebook-context-manager.ts new file mode 100644 index 0000000000000..9216285c303e4 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-context-manager.ts @@ -0,0 +1,162 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { ContextKeyChangeEvent, ContextKeyService, ContextMatcher, ScopedValueStore } from '@theia/core/lib/browser/context-key-service'; +import { DisposableCollection, Emitter } from '@theia/core'; +import { NotebookKernelService } from './notebook-kernel-service'; +import { + NOTEBOOK_CELL_EDITABLE, + NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, + NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, + NOTEBOOK_CELL_TYPE, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_SELECTED, + NOTEBOOK_OUTPUT_INPUT_FOCUSED, + NOTEBOOK_VIEW_TYPE +} from '../contributions/notebook-context-keys'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { CellKind, NotebookCellsChangeType } from '../../common'; +import { NotebookExecutionStateService } from './notebook-execution-state-service'; + +@injectable() +export class NotebookContextManager { + @inject(ContextKeyService) protected contextKeyService: ContextKeyService; + + @inject(NotebookKernelService) + protected readonly notebookKernelService: NotebookKernelService; + + @inject(NotebookExecutionStateService) + protected readonly executionStateService: NotebookExecutionStateService; + + protected readonly toDispose = new DisposableCollection(); + + protected readonly onDidChangeContextEmitter = new Emitter(); + readonly onDidChangeContext = this.onDidChangeContextEmitter.event; + + protected _context?: HTMLElement; + + scopedStore: ScopedValueStore; + + get context(): HTMLElement | undefined { + return this._context; + } + + protected cellContexts: Map> = new Map(); + + init(widget: NotebookEditorWidget): void { + this._context = widget.node; + this.scopedStore = this.contextKeyService.createScoped(widget.node); + + this.toDispose.dispose(); + + this.scopedStore.setContext(NOTEBOOK_VIEW_TYPE, widget?.notebookType); + + // Kernel related keys + const kernel = widget?.model ? this.notebookKernelService.getSelectedNotebookKernel(widget.model) : undefined; + this.scopedStore.setContext(NOTEBOOK_KERNEL_SELECTED, !!kernel); + this.scopedStore.setContext(NOTEBOOK_KERNEL, kernel?.id); + this.toDispose.push(this.notebookKernelService.onDidChangeSelectedKernel(e => { + if (e.notebook.toString() === widget?.getResourceUri()?.toString()) { + this.scopedStore.setContext(NOTEBOOK_KERNEL_SELECTED, !!e.newKernel); + this.scopedStore.setContext(NOTEBOOK_KERNEL, e.newKernel); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_KERNEL])); + } + })); + + widget.model?.onDidChangeContent(events => { + if (events.some(e => e.kind === NotebookCellsChangeType.ModelChange || e.kind === NotebookCellsChangeType.Output)) { + this.scopedStore.setContext(NOTEBOOK_HAS_OUTPUTS, widget.model?.cells.some(cell => cell.outputs.length > 0)); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_HAS_OUTPUTS])); + } + }); + + this.scopedStore.setContext(NOTEBOOK_HAS_OUTPUTS, !!widget.model?.cells.find(cell => cell.outputs.length > 0)); + + // Cell Selection related keys + this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, !!widget.model?.selectedCell); + widget.model?.onDidChangeSelectedCell(e => { + this.selectedCellChanged(e.cell); + this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, !!e); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_CELL_FOCUSED])); + }); + + this.toDispose.push(this.executionStateService.onDidChangeExecution(e => { + if (e.notebook.toString() === widget.model?.uri.toString()) { + this.setCellContext(e.cellHandle, NOTEBOOK_CELL_EXECUTING, !!e.changed); + this.setCellContext(e.cellHandle, NOTEBOOK_CELL_EXECUTION_STATE, e.changed?.state ?? 'idle'); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE])); + } + })); + + widget.onDidChangeOutputInputFocus(focus => { + this.scopedStore.setContext(NOTEBOOK_OUTPUT_INPUT_FOCUSED, focus); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_OUTPUT_INPUT_FOCUSED])); + }); + + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_VIEW_TYPE, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_KERNEL])); + } + + protected cellDisposables = new DisposableCollection(); + + selectedCellChanged(cell: NotebookCellModel | undefined): void { + this.cellDisposables.dispose(); + + this.scopedStore.setContext(NOTEBOOK_CELL_TYPE, cell ? cell.cellKind === CellKind.Code ? 'code' : 'markdown' : undefined); + + if (cell) { + this.scopedStore.setContext(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, cell.editing); + this.scopedStore.setContext(NOTEBOOK_CELL_EDITABLE, cell.cellKind === CellKind.Markup && !cell.editing); + this.cellDisposables.push(cell.onDidRequestCellEditChange(cellEdit => { + this.scopedStore.setContext(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, cellEdit); + this.scopedStore.setContext(NOTEBOOK_CELL_EDITABLE, cell.cellKind === CellKind.Markup && !cellEdit); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_CELL_MARKDOWN_EDIT_MODE])); + })); + } + + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_CELL_TYPE])); + + } + + protected setCellContext(cellHandle: number, key: string, value: unknown): void { + let cellContext = this.cellContexts.get(cellHandle); + if (!cellContext) { + cellContext = {}; + this.cellContexts.set(cellHandle, cellContext); + } + + cellContext[key] = value; + } + + getCellContext(cellHandle: number): ContextMatcher { + return this.contextKeyService.createOverlay(Object.entries(this.cellContexts.get(cellHandle) ?? {})); + } + + changeCellFocus(focus: boolean): void { + this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, focus); + } + + changeCellListFocus(focus: boolean): void { + this.scopedStore.setContext(NOTEBOOK_CELL_LIST_FOCUSED, focus); + } + + createContextKeyChangedEvent(affectedKeys: string[]): ContextKeyChangeEvent { + return { affects: keys => affectedKeys.some(key => keys.has(key)) }; + } + + dispose(): void { + this.toDispose.dispose(); + } +} diff --git a/packages/notebook/src/browser/service/notebook-editor-widget-service.ts b/packages/notebook/src/browser/service/notebook-editor-widget-service.ts index f8307cdb9bd6f..c54faa00db166 100644 --- a/packages/notebook/src/browser/service/notebook-editor-widget-service.ts +++ b/packages/notebook/src/browser/service/notebook-editor-widget-service.ts @@ -19,48 +19,49 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableCollection, Emitter } from '@theia/core'; +import { Emitter } from '@theia/core'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { ApplicationShell } from '@theia/core/lib/browser'; import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { NOTEBOOK_EDITOR_FOCUSED } from '../contributions/notebook-context-keys'; @injectable() -export class NotebookEditorWidgetService implements Disposable { +export class NotebookEditorWidgetService { @inject(ApplicationShell) protected applicationShell: ApplicationShell; - private readonly notebookEditors = new Map(); + @inject(ContextKeyService) + protected contextKeyService: ContextKeyService; - private readonly onNotebookEditorAddEmitter = new Emitter(); - private readonly onNotebookEditorRemoveEmitter = new Emitter(); + protected readonly notebookEditors = new Map(); + + protected readonly onNotebookEditorAddEmitter = new Emitter(); + protected readonly onNotebookEditorRemoveEmitter = new Emitter(); readonly onDidAddNotebookEditor = this.onNotebookEditorAddEmitter.event; readonly onDidRemoveNotebookEditor = this.onNotebookEditorRemoveEmitter.event; - private readonly onDidChangeFocusedEditorEmitter = new Emitter(); + protected readonly onDidChangeFocusedEditorEmitter = new Emitter(); readonly onDidChangeFocusedEditor = this.onDidChangeFocusedEditorEmitter.event; - private readonly toDispose = new DisposableCollection(); + protected readonly onDidChangeCurrentEditorEmitter = new Emitter(); + readonly onDidChangeCurrentEditor = this.onDidChangeCurrentEditorEmitter.event; focusedEditor?: NotebookEditorWidget = undefined; + currentEditor?: NotebookEditorWidget = undefined; + @postConstruct() protected init(): void { - this.toDispose.push(this.applicationShell.onDidChangeActiveWidget(event => { - if (event.newValue instanceof NotebookEditorWidget && event.newValue !== this.focusedEditor) { - this.focusedEditor = event.newValue; - this.onDidChangeFocusedEditorEmitter.fire(this.focusedEditor); - } else { - this.onDidChangeFocusedEditorEmitter.fire(undefined); + this.applicationShell.onDidChangeActiveWidget(event => { + this.notebookEditorFocusChanged(event.newValue as NotebookEditorWidget, event.newValue instanceof NotebookEditorWidget); + }); + this.applicationShell.onDidChangeCurrentWidget(event => { + if (event.newValue instanceof NotebookEditorWidget || event.oldValue instanceof NotebookEditorWidget) { + this.currentNotebookEditorChanged(event.newValue); } - })); - } - - dispose(): void { - this.onNotebookEditorAddEmitter.dispose(); - this.onNotebookEditorRemoveEmitter.dispose(); - this.onDidChangeFocusedEditorEmitter.dispose(); - this.toDispose.dispose(); + }); } // --- editor management @@ -71,6 +72,9 @@ export class NotebookEditorWidgetService implements Disposable { } this.notebookEditors.set(editor.id, editor); this.onNotebookEditorAddEmitter.fire(editor); + if (editor.isVisible) { + this.notebookEditorFocusChanged(editor, true); + } } removeNotebookEditor(editor: NotebookEditorWidget): void { @@ -86,8 +90,32 @@ export class NotebookEditorWidgetService implements Disposable { return this.notebookEditors.get(editorId); } - listNotebookEditors(): readonly NotebookEditorWidget[] { - return [...this.notebookEditors].map(e => e[1]); + getNotebookEditors(): readonly NotebookEditorWidget[] { + return Array.from(this.notebookEditors.values()); + } + + notebookEditorFocusChanged(editor: NotebookEditorWidget, focus: boolean): void { + if (focus) { + if (editor !== this.focusedEditor) { + this.focusedEditor = editor; + this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, true); + this.onDidChangeFocusedEditorEmitter.fire(this.focusedEditor); + } + } else if (this.focusedEditor) { + this.focusedEditor = undefined; + this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, false); + this.onDidChangeFocusedEditorEmitter.fire(undefined); + } + } + + currentNotebookEditorChanged(newEditor: unknown): void { + if (newEditor instanceof NotebookEditorWidget) { + this.currentEditor = newEditor; + this.onDidChangeCurrentEditorEmitter.fire(newEditor); + } else if (this.currentEditor?.isDisposed || !this.currentEditor?.isVisible) { + this.currentEditor = undefined; + this.onDidChangeCurrentEditorEmitter.fire(undefined); + } } } diff --git a/packages/notebook/src/browser/service/notebook-execution-service.ts b/packages/notebook/src/browser/service/notebook-execution-service.ts index abfbbaaf4795e..643ea2ae6785a 100644 --- a/packages/notebook/src/browser/service/notebook-execution-service.ts +++ b/packages/notebook/src/browser/service/notebook-execution-service.ts @@ -23,11 +23,10 @@ import { CellExecution, NotebookExecutionStateService } from '../service/noteboo import { CellKind, NotebookCellExecutionState } from '../../common'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { NotebookModel } from '../view-model/notebook-model'; -import { NotebookKernelService, NotebookKernel } from './notebook-kernel-service'; +import { NotebookKernelService } from './notebook-kernel-service'; import { CommandService, Disposable } from '@theia/core'; -import { NotebookKernelQuickPickService, NotebookKernelQuickPickServiceImpl } from './notebook-kernel-quick-pick-service'; +import { NotebookKernelQuickPickService } from './notebook-kernel-quick-pick-service'; import { NotebookKernelHistoryService } from './notebook-kernel-history-service'; -import { NotebookCommands } from '../contributions/notebook-actions-contribution'; export interface CellExecutionParticipant { onWillExecuteCell(executions: CellExecution[]): Promise; @@ -49,9 +48,9 @@ export class NotebookExecutionService { protected commandService: CommandService; @inject(NotebookKernelQuickPickService) - protected notebookKernelQuickPickService: NotebookKernelQuickPickServiceImpl; + protected notebookKernelQuickPickService: NotebookKernelQuickPickService; - private readonly cellExecutionParticipants = new Set(); + protected readonly cellExecutionParticipants = new Set(); async executeNotebookCells(notebook: NotebookModel, cells: Iterable): Promise { const cellsArr = Array.from(cells) @@ -69,7 +68,7 @@ export class NotebookExecutionService { } } - const kernel = await this.resolveKernel(notebook); + const kernel = await this.notebookKernelHistoryService.resolveSelectedKernel(notebook); if (!kernel) { // clear all pending cell executions @@ -89,6 +88,15 @@ export class NotebookExecutionService { // request execution if (validCellExecutions.length > 0) { + const cellRemoveListener = notebook.onDidAddOrRemoveCell(e => { + if (e.rawEvent.changes.some(c => c.deleteCount > 0)) { + const executionsToCancel = validCellExecutions.filter(exec => !notebook.cells.find(cell => cell.handle === exec.cellHandle)); + if (executionsToCancel.length > 0) { + kernel.cancelNotebookCellExecution(notebook.uri, executionsToCancel.map(c => c.cellHandle)); + executionsToCancel.forEach(exec => exec.complete({})); + } + } + }); await this.runExecutionParticipants(validCellExecutions); this.notebookKernelService.selectKernelForNotebook(kernel, notebook); @@ -98,6 +106,9 @@ export class NotebookExecutionService { if (unconfirmed.length) { unconfirmed.forEach(exe => exe.complete({})); } + + cellRemoveListener.dispose(); + } } @@ -106,7 +117,7 @@ export class NotebookExecutionService { return Disposable.create(() => this.cellExecutionParticipants.delete(participant)); } - private async runExecutionParticipants(executions: CellExecution[]): Promise { + protected async runExecutionParticipants(executions: CellExecution[]): Promise { for (const participant of this.cellExecutionParticipants) { await participant.onWillExecuteCell(executions); } @@ -125,15 +136,4 @@ export class NotebookExecutionService { this.cancelNotebookCellHandles(notebook, Array.from(cells, cell => cell.handle)); } - async resolveKernel(notebook: NotebookModel): Promise { - const alreadySelected = this.notebookKernelHistoryService.getKernels(notebook); - - if (alreadySelected.selected) { - return alreadySelected.selected; - } - - await this.commandService.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, notebook); - const { selected } = this.notebookKernelHistoryService.getKernels(notebook); - return selected; - } } diff --git a/packages/notebook/src/browser/service/notebook-execution-state-service.ts b/packages/notebook/src/browser/service/notebook-execution-state-service.ts index 51ed8dc2059c5..88e44be22e0f0 100644 --- a/packages/notebook/src/browser/service/notebook-execution-state-service.ts +++ b/packages/notebook/src/browser/service/notebook-execution-state-service.ts @@ -18,15 +18,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableCollection, Emitter, URI } from '@theia/core'; +import { Disposable, DisposableCollection, Emitter, URI, generateUuid } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { NotebookService } from './notebook-service'; import { CellEditType, CellExecuteOutputEdit, CellExecuteOutputItemEdit, CellExecutionUpdateType, - CellUri, CellPartialInternalMetadataEditByHandle, NotebookCellExecutionState, CellEditOperation, NotebookCellInternalMetadata + CellUri, NotebookCellExecutionState, NotebookCellInternalMetadata } from '../../common'; +import { CellPartialInternalMetadataEditByHandle, CellEditOperation } from '../notebook-types'; import { NotebookModel } from '../view-model/notebook-model'; -import { v4 } from 'uuid'; export type CellExecuteUpdate = CellExecuteOutputEdit | CellExecuteOutputItemEdit | CellExecutionStateUpdate; @@ -43,10 +43,6 @@ export interface CellExecutionStateUpdate { isPaused?: boolean; } -export interface ICellExecutionComplete { - runEndTime?: number; - lastRunSuccess?: boolean; -} export enum NotebookExecutionType { cell, notebook @@ -73,10 +69,10 @@ export class NotebookExecutionStateService implements Disposable { protected readonly executions = new Map>(); - private readonly onDidChangeExecutionEmitter = new Emitter(); + protected readonly onDidChangeExecutionEmitter = new Emitter(); onDidChangeExecution = this.onDidChangeExecutionEmitter.event; - private readonly onDidChangeLastRunFailStateEmitter = new Emitter(); + protected readonly onDidChangeLastRunFailStateEmitter = new Emitter(); onDidChangeLastRunFailState = this.onDidChangeLastRunFailStateEmitter.event; getOrCreateCellExecution(notebookUri: URI, cellHandle: number): CellExecution { @@ -102,7 +98,7 @@ export class NotebookExecutionStateService implements Disposable { } - private createNotebookCellExecution(notebook: NotebookModel, cellHandle: number): CellExecution { + protected createNotebookCellExecution(notebook: NotebookModel, cellHandle: number): CellExecution { const notebookUri = notebook.uri; const execution = new CellExecution(cellHandle, notebook); execution.toDispose.push(execution.onDidUpdate(() => this.onDidChangeExecutionEmitter.fire(new CellExecutionStateChangedEvent(notebookUri, cellHandle, execution)))); @@ -111,7 +107,7 @@ export class NotebookExecutionStateService implements Disposable { return execution; } - private onCellExecutionDidComplete(notebookUri: URI, cellHandle: number, exe: CellExecution, lastRunSuccess?: boolean): void { + protected onCellExecutionDidComplete(notebookUri: URI, cellHandle: number, exe: CellExecution, lastRunSuccess?: boolean): void { const notebookExecutions = this.executions.get(notebookUri.toString())?.get(cellHandle); if (!notebookExecutions) { throw new Error('Notebook Cell Execution not found while trying to complete it'); @@ -142,15 +138,15 @@ export class NotebookExecutionStateService implements Disposable { } export class CellExecution implements Disposable { - private readonly onDidUpdateEmitter = new Emitter(); + protected readonly onDidUpdateEmitter = new Emitter(); readonly onDidUpdate = this.onDidUpdateEmitter.event; - private readonly onDidCompleteEmitter = new Emitter(); + protected readonly onDidCompleteEmitter = new Emitter(); readonly onDidComplete = this.onDidCompleteEmitter.event; toDispose = new DisposableCollection(); - private _state: NotebookCellExecutionState = NotebookCellExecutionState.Unconfirmed; + protected _state: NotebookCellExecutionState = NotebookCellExecutionState.Unconfirmed; get state(): NotebookCellExecutionState { return this._state; } @@ -159,19 +155,19 @@ export class CellExecution implements Disposable { return this.notebook.uri; } - private _didPause = false; + protected _didPause = false; get didPause(): boolean { return this._didPause; } - private _isPaused = false; + protected _isPaused = false; get isPaused(): boolean { return this._isPaused; } constructor( readonly cellHandle: number, - private readonly notebook: NotebookModel, + protected readonly notebook: NotebookModel, ) { console.debug(`CellExecution#ctor ${this.getCellLog()}`); } @@ -181,7 +177,7 @@ export class CellExecution implements Disposable { editType: CellEditType.PartialInternalMetadata, handle: this.cellHandle, internalMetadata: { - executionId: v4(), + executionId: generateUuid(), runStartTime: undefined, runEndTime: undefined, lastRunSuccess: undefined, @@ -192,7 +188,7 @@ export class CellExecution implements Disposable { this.applyCellExecutionEditsToNotebook([startExecuteEdit]); } - private getCellLog(): string { + protected getCellLog(): string { return `${this.notebookURI.toString()}, ${this.cellHandle}`; } @@ -258,7 +254,7 @@ export class CellExecution implements Disposable { this.toDispose.dispose(); } - private applyCellExecutionEditsToNotebook(edits: CellEditOperation[]): void { + protected applyCellExecutionEditsToNotebook(edits: CellEditOperation[]): void { this.notebook.applyEdits(edits, false); } } diff --git a/packages/notebook/src/browser/service/notebook-kernel-history-service.ts b/packages/notebook/src/browser/service/notebook-kernel-history-service.ts index a28cba3bbccc4..70a3c79389976 100644 --- a/packages/notebook/src/browser/service/notebook-kernel-history-service.ts +++ b/packages/notebook/src/browser/service/notebook-kernel-history-service.ts @@ -21,7 +21,9 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { StorageService } from '@theia/core/lib/browser'; import { NotebookKernel, NotebookTextModelLike, NotebookKernelService } from './notebook-kernel-service'; -import { Disposable } from '@theia/core'; +import { CommandService, Disposable } from '@theia/core'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookCommands } from '../contributions/notebook-actions-contribution'; interface KernelsList { [viewType: string]: string[]; @@ -43,10 +45,11 @@ export class NotebookKernelHistoryService implements Disposable { @inject(NotebookKernelService) protected notebookKernelService: NotebookKernelService; - declare serviceBrand: undefined; + @inject(CommandService) + protected commandService: CommandService; - private static STORAGE_KEY = 'notebook.kernelHistory'; - private mostRecentKernelsMap: KernelsList = {}; + protected static STORAGE_KEY = 'notebook.kernelHistory'; + protected mostRecentKernelsMap: KernelsList = {}; @postConstruct() protected init(): void { @@ -68,6 +71,18 @@ export class NotebookKernelHistoryService implements Disposable { }; } + async resolveSelectedKernel(notebook: NotebookModel): Promise { + const alreadySelected = this.getKernels(notebook); + + if (alreadySelected.selected) { + return alreadySelected.selected; + } + + await this.commandService.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, notebook); + const { selected } = this.getKernels(notebook); + return selected; + } + addMostRecentKernel(kernel: NotebookKernel): void { const viewType = kernel.viewType; const recentKernels = this.mostRecentKernelsMap[viewType] ?? [kernel.id]; @@ -80,16 +95,16 @@ export class NotebookKernelHistoryService implements Disposable { this.saveState(); } - private saveState(): void { + protected saveState(): void { let notEmpty = false; - for (const [_, kernels] of Object.entries(this.mostRecentKernelsMap)) { + for (const kernels of Object.values(this.mostRecentKernelsMap)) { notEmpty = notEmpty || Object.entries(kernels).length > 0; } this.storageService.setData(NotebookKernelHistoryService.STORAGE_KEY, notEmpty ? this.mostRecentKernelsMap : undefined); } - private async loadState(): Promise { + protected async loadState(): Promise { const kernelMap = await this.storageService.getData(NotebookKernelHistoryService.STORAGE_KEY); if (kernelMap) { this.mostRecentKernelsMap = kernelMap as KernelsList; diff --git a/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts b/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts index 83a123082f884..e4285eea344f7 100644 --- a/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts +++ b/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts @@ -18,19 +18,18 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ArrayUtils, Command, CommandService, DisposableCollection, Event, nls, QuickInputButton, QuickInputService, QuickPickInput, QuickPickItem, URI, } from '@theia/core'; +import { ArrayUtils, CommandService, DisposableCollection, Event, nls, QuickInputButton, QuickInputService, QuickPickInput, QuickPickItem, URI, } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { NotebookKernelService, NotebookKernel, NotebookKernelMatchResult, SourceCommand } from './notebook-kernel-service'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookEditorWidget } from '../notebook-editor-widget'; import { codicon, OpenerService } from '@theia/core/lib/browser'; import { NotebookKernelHistoryService } from './notebook-kernel-history-service'; +import { NotebookCommand, NotebookModelResource } from '../../common'; import debounce = require('@theia/core/shared/lodash.debounce'); export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; -export const NotebookKernelQuickPickService = Symbol('NotebookKernelQuickPickService'); - type KernelPick = QuickPickItem & { kernel: NotebookKernel }; function isKernelPick(item: QuickPickInput): item is KernelPick { return 'kernel' in item; @@ -45,7 +44,7 @@ function isSourcePick(item: QuickPickInput): item is SourcePick { } type InstallExtensionPick = QuickPickItem & { extensionIds: string[] }; -type KernelSourceQuickPickItem = QuickPickItem & { command: Command; documentation?: string }; +type KernelSourceQuickPickItem = QuickPickItem & { command: NotebookCommand; documentation?: string }; function isKernelSourceQuickPickItem(item: QuickPickItem): item is KernelSourceQuickPickItem { return 'command' in item; } @@ -82,7 +81,7 @@ function toKernelQuickPick(kernel: NotebookKernel, selected: NotebookKernel | un } @injectable() -export abstract class NotebookKernelQuickPickServiceImpl { +export class NotebookKernelQuickPickService { @inject(NotebookKernelService) protected readonly notebookKernelService: NotebookKernelService; @@ -91,6 +90,12 @@ export abstract class NotebookKernelQuickPickServiceImpl { @inject(CommandService) protected readonly commandService: CommandService; + @inject(OpenerService) + protected openerService: OpenerService; + + @inject(NotebookKernelHistoryService) + protected notebookKernelHistoryService: NotebookKernelHistoryService; + async showQuickPick(editor: NotebookModel, wantedId?: string, skipAutoRun?: boolean): Promise { const notebook = editor; const matchResult = this.getMatchingResult(notebook); @@ -200,40 +205,6 @@ export abstract class NotebookKernelQuickPickServiceImpl { return false; } - protected getMatchingResult(notebook: NotebookModel): NotebookKernelMatchResult { - return this.notebookKernelService.getMatchingKernel(notebook); - } - - protected abstract getKernelPickerQuickPickItems(matchResult: NotebookKernelMatchResult): QuickPickInput[]; - - protected async handleQuickPick(editor: NotebookModel, pick: KernelQuickPickItem, quickPickItems: KernelQuickPickItem[]): Promise { - if (isKernelPick(pick)) { - const newKernel = pick.kernel; - this.selectKernel(editor, newKernel); - return true; - } - - if (isSourcePick(pick)) { - // selected explicitly, it should trigger the execution? - pick.action.run(this.commandService); - } - - return true; - } - - protected selectKernel(notebook: NotebookModel, kernel: NotebookKernel): void { - this.notebookKernelService.selectKernelForNotebook(kernel, notebook); - } -} -@injectable() -export class KernelPickerMRUStrategy extends NotebookKernelQuickPickServiceImpl { - - @inject(OpenerService) - protected openerService: OpenerService; - - @inject(NotebookKernelHistoryService) - protected notebookKernelHistoryService: NotebookKernelHistoryService; - protected getKernelPickerQuickPickItems(matchResult: NotebookKernelMatchResult): QuickPickInput[] { const quickPickItems: QuickPickInput[] = []; @@ -266,17 +237,17 @@ export class KernelPickerMRUStrategy extends NotebookKernelQuickPickServiceImpl return quickPickItems; } - protected override selectKernel(notebook: NotebookModel, kernel: NotebookKernel): void { + protected selectKernel(notebook: NotebookModel, kernel: NotebookKernel): void { const currentInfo = this.notebookKernelService.getMatchingKernel(notebook); if (currentInfo.selected) { // there is already a selected kernel this.notebookKernelHistoryService.addMostRecentKernel(currentInfo.selected); } - super.selectKernel(notebook, kernel); + this.notebookKernelService.selectKernelForNotebook(kernel, notebook); this.notebookKernelHistoryService.addMostRecentKernel(kernel); } - protected override getMatchingResult(notebook: NotebookModel): NotebookKernelMatchResult { + protected getMatchingResult(notebook: NotebookModel): NotebookKernelMatchResult { const { selected, all } = this.notebookKernelHistoryService.getKernels(notebook); const matchingResult = this.notebookKernelService.getMatchingKernel(notebook); return { @@ -287,15 +258,26 @@ export class KernelPickerMRUStrategy extends NotebookKernelQuickPickServiceImpl }; } - protected override async handleQuickPick(editor: NotebookModel, pick: KernelQuickPickItem, items: KernelQuickPickItem[]): Promise { + protected async handleQuickPick(editor: NotebookModel, pick: KernelQuickPickItem, items: KernelQuickPickItem[]): Promise { if (pick.id === 'selectAnother') { return this.displaySelectAnotherQuickPick(editor, items.length === 1 && items[0] === pick); } - return super.handleQuickPick(editor, pick, items); + if (isKernelPick(pick)) { + const newKernel = pick.kernel; + this.selectKernel(editor, newKernel); + return true; + } + + if (isSourcePick(pick)) { + // selected explicitly, it should trigger the execution? + pick.action.run(this.commandService); + } + + return true; } - private async displaySelectAnotherQuickPick(editor: NotebookModel, kernelListEmpty: boolean): Promise { + protected async displaySelectAnotherQuickPick(editor: NotebookModel, kernelListEmpty: boolean): Promise { const notebook: NotebookModel = editor; const disposables = new DisposableCollection(); const quickPick = this.quickInputService.createQuickPick(); @@ -374,6 +356,7 @@ export class KernelPickerMRUStrategy extends NotebookKernelQuickPickServiceImpl return this.displaySelectAnotherQuickPick(editor, false); } } catch (ex) { + console.error('Failed to select notebook kernel', ex); return false; } } else if (isKernelPick(selectedKernelPickItem)) { @@ -388,6 +371,7 @@ export class KernelPickerMRUStrategy extends NotebookKernelQuickPickServiceImpl await selectedKernelPickItem.action.run(this.commandService); return true; } catch (ex) { + console.error('Failed to select notebook kernel', ex); return false; } } @@ -415,11 +399,11 @@ export class KernelPickerMRUStrategy extends NotebookKernelQuickPickServiceImpl return false; } - private isUri(value: string): boolean { + protected isUri(value: string): boolean { return /^(?\w[\w\d+.-]*):/.test(value); } - private async calculateKernelSources(editor: NotebookModel): Promise[]> { + protected async calculateKernelSources(editor: NotebookModel): Promise[]> { const notebook: NotebookModel = editor; const actions = await this.notebookKernelService.getKernelSourceActionsFromProviders(notebook); @@ -464,7 +448,7 @@ export class KernelPickerMRUStrategy extends NotebookKernelQuickPickServiceImpl return quickPickItems; } - private async selectOneKernel(notebook: NotebookModel, source: string, kernels: NotebookKernel[]): Promise { + protected async selectOneKernel(notebook: NotebookModel, source: string, kernels: NotebookKernel[]): Promise { const quickPickItems: QuickPickInput[] = kernels.map(kernel => toKernelQuickPick(kernel, undefined)); const quickPick = this.quickInputService.createQuickPick(); quickPick.items = quickPickItems; @@ -488,10 +472,8 @@ export class KernelPickerMRUStrategy extends NotebookKernelQuickPickServiceImpl quickPick.show(); } - private async executeCommand(notebook: NotebookModel, command: string | Command): Promise { - const id = typeof command === 'string' ? command : command.id; - - return this.commandService.executeCommand(id, { uri: notebook.uri }); - + protected async executeCommand(notebook: NotebookModel, command: NotebookCommand): Promise { + const args = (command.arguments || []).concat([NotebookModelResource.create(notebook.uri)]); + return this.commandService.executeCommand(command.id, ...args); } } diff --git a/packages/notebook/src/browser/service/notebook-kernel-service.ts b/packages/notebook/src/browser/service/notebook-kernel-service.ts index f5e412bd52c4d..86a1159757051 100644 --- a/packages/notebook/src/browser/service/notebook-kernel-service.ts +++ b/packages/notebook/src/browser/service/notebook-kernel-service.ts @@ -54,6 +54,11 @@ export interface NotebookKernel { // ID of the extension providing this kernel readonly extensionId: string; + readonly localResourceRoot: URI; + readonly preloadUris: URI[]; + readonly preloadProvides: string[]; + + readonly handle: number; label: string; description?: string; detail?: string; @@ -79,7 +84,7 @@ export interface NotebookTextModelLike { uri: URI; viewType: string } class KernelInfo { - private static instanceCounter = 0; + protected static instanceCounter = 0; score: number; readonly kernel: NotebookKernel; @@ -125,7 +130,7 @@ export class SourceCommand implements Disposable { this.onDidChangeStateEmitter.fire(); } - private async runCommand(commandService: CommandService): Promise { + protected async runCommand(commandService: CommandService): Promise { try { await commandService.executeCommand(this.command.id, { uri: this.model.uri, @@ -144,7 +149,7 @@ export class SourceCommand implements Disposable { const NOTEBOOK_KERNEL_BINDING_STORAGE_KEY = 'notebook.kernel.bindings'; @injectable() -export class NotebookKernelService implements Disposable { +export class NotebookKernelService { @inject(NotebookService) protected notebookService: NotebookService; @@ -152,33 +157,34 @@ export class NotebookKernelService implements Disposable { @inject(StorageService) protected storageService: StorageService; - private readonly kernels = new Map(); + protected readonly kernels = new Map(); - private notebookBindings: { [key: string]: string } = {}; + protected notebookBindings: Record = {}; - private readonly kernelDetectionTasks = new Map(); - private readonly onDidChangeKernelDetectionTasksEmitter = new Emitter(); + protected readonly kernelDetectionTasks = new Map(); + protected readonly onDidChangeKernelDetectionTasksEmitter = new Emitter(); readonly onDidChangeKernelDetectionTasks = this.onDidChangeKernelDetectionTasksEmitter.event; - private readonly onDidChangeSourceActionsEmitter = new Emitter(); - private readonly kernelSourceActionProviders = new Map(); + protected readonly onDidChangeSourceActionsEmitter = new Emitter(); + protected readonly kernelSourceActionProviders = new Map(); readonly onDidChangeSourceActions: Event = this.onDidChangeSourceActionsEmitter.event; - private readonly onDidAddKernelEmitter = new Emitter(); + protected readonly onDidAddKernelEmitter = new Emitter(); readonly onDidAddKernel: Event = this.onDidAddKernelEmitter.event; - private readonly onDidRemoveKernelEmitter = new Emitter(); + protected readonly onDidRemoveKernelEmitter = new Emitter(); readonly onDidRemoveKernel: Event = this.onDidRemoveKernelEmitter.event; - private readonly onDidChangeSelectedNotebookKernelBindingEmitter = new Emitter(); + protected readonly onDidChangeSelectedNotebookKernelBindingEmitter = new Emitter(); readonly onDidChangeSelectedKernel: Event = this.onDidChangeSelectedNotebookKernelBindingEmitter.event; - private readonly onDidChangeNotebookAffinityEmitter = new Emitter(); + protected readonly onDidChangeNotebookAffinityEmitter = new Emitter(); readonly onDidChangeNotebookAffinity: Event = this.onDidChangeNotebookAffinityEmitter.event; @postConstruct() init(): void { - this.storageService.getData(NOTEBOOK_KERNEL_BINDING_STORAGE_KEY).then((value: { [key: string]: string } | undefined) => { + this.notebookService.onDidAddNotebookDocument(model => this.tryAutoBindNotebook(model)); + this.storageService.getData(NOTEBOOK_KERNEL_BINDING_STORAGE_KEY).then((value: Record | undefined) => { if (value) { this.notebookBindings = value; } @@ -233,14 +239,18 @@ export class NotebookKernelService implements Disposable { const all = kernels.map(obj => obj.kernel); // bound kernel - const selectedId = this.notebookBindings[`${notebook.viewType}/${notebook.uri}`]; - const selected = selectedId ? this.kernels.get(selectedId)?.kernel : undefined; + const selected = this.getSelectedNotebookKernel(notebook); const suggestions = kernels.filter(item => item.instanceAffinity > 1).map(item => item.kernel); // TODO implement notebookAffinity const hidden = kernels.filter(item => item.instanceAffinity < 0).map(item => item.kernel); return { all, selected, suggestions, hidden }; } + getSelectedNotebookKernel(notebook: NotebookTextModelLike): NotebookKernel | undefined { + const selectedId = this.notebookBindings[`${notebook.viewType}/${notebook.uri}`]; + return selectedId ? this.kernels.get(selectedId)?.kernel : undefined; + } + selectKernelForNotebook(kernel: NotebookKernel | undefined, notebook: NotebookTextModelLike): void { const key = `${notebook.viewType}/${notebook.uri}`; const oldKernel = this.notebookBindings[key]; @@ -268,7 +278,7 @@ export class NotebookKernelService implements Disposable { return this.kernels.get(id)?.kernel; } - private static score(kernel: NotebookKernel, notebook: NotebookTextModelLike): number { + protected static score(kernel: NotebookKernel, notebook: NotebookTextModelLike): number { if (kernel.viewType === notebook.viewType) { return 10; } else if (kernel.viewType === '*') { @@ -278,7 +288,7 @@ export class NotebookKernelService implements Disposable { } } - private tryAutoBindNotebook(notebook: NotebookModel, onlyThisKernel?: NotebookKernel): void { + protected tryAutoBindNotebook(notebook: NotebookModel, onlyThisKernel?: NotebookKernel): void { const id = this.notebookBindings[`${notebook.viewType}/${notebook.uri}`]; if (!id) { @@ -344,13 +354,4 @@ export class NotebookKernelService implements Disposable { const allActions = await Promise.all(promises); return allActions.flat(); } - - dispose(): void { - this.onDidChangeKernelDetectionTasksEmitter.dispose(); - this.onDidChangeSourceActionsEmitter.dispose(); - this.onDidAddKernelEmitter.dispose(); - this.onDidRemoveKernelEmitter.dispose(); - this.onDidChangeSelectedNotebookKernelBindingEmitter.dispose(); - this.onDidChangeNotebookAffinityEmitter.dispose(); - } } diff --git a/packages/notebook/src/browser/service/notebook-model-resolver-service.ts b/packages/notebook/src/browser/service/notebook-model-resolver-service.ts index 03f42c4997701..949fc0b779621 100644 --- a/packages/notebook/src/browser/service/notebook-model-resolver-service.ts +++ b/packages/notebook/src/browser/service/notebook-model-resolver-service.ts @@ -14,11 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Emitter, URI } from '@theia/core'; +import { Emitter, Resource, ResourceProvider, UNTITLED_SCHEME, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { UriComponents } from '@theia/core/lib/common/uri'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; -import { CellKind, NotebookData } from '../../common'; +import { NotebookData } from '../../common'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookService } from './notebook-service'; import { NotebookTypeRegistry } from '../notebook-type-registry'; @@ -28,12 +28,16 @@ import { match } from '@theia/core/lib/common/glob'; export interface UntitledResource { untitledResource: URI | undefined } + @injectable() export class NotebookModelResolverService { @inject(FileService) protected fileService: FileService; + @inject(ResourceProvider) + protected resourceProvider: ResourceProvider; + @inject(NotebookService) protected notebookService: NotebookService; @@ -46,23 +50,24 @@ export class NotebookModelResolverService { readonly onDidSaveNotebook = this.onDidSaveNotebookEmitter.event; async resolve(resource: URI, viewType?: string): Promise { - + const existingModel = this.notebookService.getNotebookEditorModel(resource); if (!viewType) { - const existingViewType = this.notebookService.getNotebookEditorModel(resource)?.viewType; - if (existingViewType) { - viewType = existingViewType; + if (existingModel) { + return existingModel; } else { viewType = this.findViewTypeForResource(resource); } + } else if (existingModel?.viewType === viewType) { + return existingModel; } if (!viewType) { throw new Error(`Missing viewType for '${resource}'`); } - const notebookData = await this.resolveExistingNotebookData(resource, viewType!); - - const notebookModel = await this.notebookService.createNotebookModel(notebookData, viewType, resource); + const actualResource = await this.resourceProvider(resource); + const notebookData = await this.resolveExistingNotebookData(actualResource, viewType!); + const notebookModel = await this.notebookService.createNotebookModel(notebookData, viewType, actualResource); notebookModel.onDirtyChanged(() => this.onDidChangeDirtyEmitter.fire(notebookModel)); notebookModel.onDidSaveNotebook(() => this.onDidSaveNotebookEmitter.fire(notebookModel.uri.toComponents())); @@ -83,7 +88,7 @@ export class NotebookModelResolverService { const suffix = this.getPossibleFileEnding(notebookTypeInfo.selector ?? []) ?? ''; for (let counter = 1; ; counter++) { const candidate = new URI() - .withScheme('untitled') + .withScheme(UNTITLED_SCHEME) .withPath(`Untitled-notebook-${counter}${suffix}`) .withQuery(viewType); if (!this.notebookService.getNotebookEditorModel(candidate)) { @@ -91,7 +96,7 @@ export class NotebookModelResolverService { break; } } - } else if (arg.untitledResource.scheme === 'untitled') { + } else if (arg.untitledResource.scheme === UNTITLED_SCHEME) { resource = arg.untitledResource; } else { throw new Error('Invalid untitled resource: ' + arg.untitledResource.toString() + ' untitled resources with associated file path are not supported yet'); @@ -103,25 +108,18 @@ export class NotebookModelResolverService { return this.resolve(resource, viewType); } - protected async resolveExistingNotebookData(resource: URI, viewType: string): Promise { - if (resource.scheme === 'untitled') { - + async resolveExistingNotebookData(resource: Resource, viewType: string): Promise { + if (resource.uri.scheme === 'untitled') { return { - cells: [ - { - cellKind: CellKind.Markup, - language: 'markdown', - outputs: [], - source: '' - } - ], + cells: [], metadata: {} }; } else { - const file = await this.fileService.readFile(resource); - - const dataProvider = await this.notebookService.getNotebookDataProvider(viewType); - const notebook = await dataProvider.serializer.toNotebook(file.value); + const [dataProvider, contents] = await Promise.all([ + this.notebookService.getNotebookDataProvider(viewType), + this.fileService.readFile(resource.uri) + ]); + const notebook = await dataProvider.serializer.toNotebook(contents.value); return notebook; } diff --git a/packages/notebook/src/browser/service/notebook-monaco-text-model-service.ts b/packages/notebook/src/browser/service/notebook-monaco-text-model-service.ts new file mode 100644 index 0000000000000..3d43a4bb5988b --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-monaco-text-model-service.ts @@ -0,0 +1,48 @@ +// ***************************************************************************** +// 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 { ReferenceCollection, URI, Reference, Event } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; +import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { NotebookModel } from '../view-model/notebook-model'; + +/** + * special service for creating monaco textmodels for notebook cells. + * Its for optimization purposes since there is alot of overhead otherwise with calling the backend to create a document for each cell and other smaller things. + */ +@injectable() +export class NotebookMonacoTextModelService { + + @inject(MonacoTextModelService) + protected readonly monacoTextModelService: MonacoTextModelService; + + protected readonly cellmodels = new ReferenceCollection( + uri => this.monacoTextModelService.createUnmanagedModel(new URI(uri)) + ); + + getOrCreateNotebookCellModelReference(uri: URI): Promise> { + return this.cellmodels.acquire(uri.toString()); + } + + async createTextModelsForNotebook(notebook: NotebookModel): Promise { + await Promise.all(notebook.cells.map(cell => cell.resolveTextModel())); + } + + get onDidCreateNotebookCellModel(): Event { + return this.cellmodels.onDidCreate; + } +} diff --git a/packages/notebook/src/browser/service/notebook-options.ts b/packages/notebook/src/browser/service/notebook-options.ts new file mode 100644 index 0000000000000..2ecf59f02f31f --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-options.ts @@ -0,0 +1,155 @@ + +// ***************************************************************************** +// 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { PreferenceService } from '@theia/core/lib/browser'; +import { Emitter } from '@theia/core'; +import { NotebookPreferences, notebookPreferenceSchema } from '../contributions/notebook-preferences'; +import { EditorPreferences } from '@theia/editor/lib/browser'; +import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; +import { PixelRatio } from '@theia/monaco-editor-core/esm/vs/base/browser/browser'; + +const notebookOutputOptionsRelevantPreferences = [ + 'editor.fontSize', + 'editor.fontFamily', + NotebookPreferences.NOTEBOOK_LINE_NUMBERS, + NotebookPreferences.OUTPUT_LINE_HEIGHT, + NotebookPreferences.OUTPUT_FONT_SIZE, + NotebookPreferences.OUTPUT_FONT_FAMILY, + NotebookPreferences.OUTPUT_SCROLLING, + NotebookPreferences.OUTPUT_WORD_WRAP, + NotebookPreferences.OUTPUT_LINE_LIMIT +]; + +export interface NotebookOutputOptions { + // readonly outputNodePadding: number; + readonly outputNodeLeftPadding: number; + // readonly previewNodePadding: number; + // readonly markdownLeftMargin: number; + // readonly leftMargin: number; + // readonly rightMargin: number; + // readonly runGutter: number; + // readonly dragAndDropEnabled: boolean; + readonly fontSize: number; + readonly outputFontSize?: number; + readonly fontFamily: string; + readonly outputFontFamily?: string; + // readonly markupFontSize: number; + // readonly markdownLineHeight: number; + readonly outputLineHeight: number; + readonly outputScrolling: boolean; + readonly outputWordWrap: boolean; + readonly outputLineLimit: number; + // readonly outputLinkifyFilePaths: boolean; + // readonly minimalError: boolean; + +} + +@injectable() +export class NotebookOptionsService { + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + @inject(EditorPreferences) + protected readonly editorPreferences: EditorPreferences; + + protected outputOptionsChangedEmitter = new Emitter(); + onDidChangeOutputOptions = this.outputOptionsChangedEmitter.event; + + protected fontInfo?: BareFontInfo; + get editorFontInfo(): BareFontInfo { + return this.getOrCreateMonacoFontInfo(); + } + + @postConstruct() + protected init(): void { + this.preferenceService.onPreferencesChanged(async preferenceChanges => { + if (notebookOutputOptionsRelevantPreferences.some(p => p in preferenceChanges)) { + this.outputOptionsChangedEmitter.fire(this.computeOutputOptions()); + } + }); + } + + computeOutputOptions(): NotebookOutputOptions { + const outputLineHeight = this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_LINE_HEIGHT); + + const fontSize = this.preferenceService.get('editor.fontSize')!; + const outputFontSize = this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_FONT_SIZE); + + return { + fontSize, + outputFontSize: outputFontSize, + fontFamily: this.preferenceService.get('editor.fontFamily')!, + outputNodeLeftPadding: 8, + outputFontFamily: this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_FONT_FAMILY), + outputLineHeight: this.computeOutputLineHeight(outputLineHeight, outputFontSize ?? fontSize), + outputScrolling: this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_SCROLLING)!, + outputWordWrap: this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_WORD_WRAP)!, + outputLineLimit: this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_LINE_LIMIT)! + }; + } + + protected getNotebookPreferenceWithDefault(key: string): T { + return this.preferenceService.get(key, notebookPreferenceSchema.properties?.[key]?.default as T); + } + + protected computeOutputLineHeight(lineHeight: number, outputFontSize: number): number { + const minimumLineHeight = 9; + + if (lineHeight === 0) { + // use editor line height + lineHeight = this.editorFontInfo.lineHeight; + } else if (lineHeight < minimumLineHeight) { + // Values too small to be line heights in pixels are in ems. + let fontSize = outputFontSize; + if (fontSize === 0) { + fontSize = this.preferenceService.get('editor.fontSize')!; + } + + lineHeight = lineHeight * fontSize; + } + + // Enforce integer, minimum constraints + lineHeight = Math.round(lineHeight); + if (lineHeight < minimumLineHeight) { + lineHeight = minimumLineHeight; + } + + return lineHeight; + } + + protected getOrCreateMonacoFontInfo(): BareFontInfo { + if (!this.fontInfo) { + this.fontInfo = this.createFontInfo(); + this.editorPreferences.onPreferenceChanged(e => this.fontInfo = this.createFontInfo()); + } + return this.fontInfo; + } + + protected createFontInfo(): BareFontInfo { + return BareFontInfo.createFromRawSettings({ + fontFamily: this.editorPreferences['editor.fontFamily'], + fontWeight: String(this.editorPreferences['editor.fontWeight']), + fontSize: this.editorPreferences['editor.fontSize'], + fontLigatures: this.editorPreferences['editor.fontLigatures'], + lineHeight: this.editorPreferences['editor.lineHeight'], + letterSpacing: this.editorPreferences['editor.letterSpacing'], + }, PixelRatio.value); + } + +} diff --git a/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts b/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts index db2072ba1f420..ba35e516265b8 100644 --- a/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts +++ b/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts @@ -19,8 +19,9 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from '@theia/core'; -import { injectable } from '@theia/core/shared/inversify'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; +import { NotebookEditorWidgetService } from './notebook-editor-widget-service'; interface RendererMessage { editorId: string; @@ -44,14 +45,17 @@ export interface RendererMessaging extends Disposable { @injectable() export class NotebookRendererMessagingService implements Disposable { - private readonly postMessageEmitter = new Emitter(); + protected readonly postMessageEmitter = new Emitter(); readonly onPostMessage = this.postMessageEmitter.event; - private readonly willActivateRendererEmitter = new Emitter(); + protected readonly willActivateRendererEmitter = new Emitter(); readonly onWillActivateRenderer = this.willActivateRendererEmitter.event; - private readonly activations = new Map(); - private readonly scopedMessaging = new Map(); + @inject(NotebookEditorWidgetService) + protected readonly editorWidgetService: NotebookEditorWidgetService; + + protected readonly activations = new Map(); + protected readonly scopedMessaging = new Map(); receiveMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise { if (editorId === undefined) { @@ -86,6 +90,10 @@ export class NotebookRendererMessagingService implements Disposable { const messaging: RendererMessaging = { postMessage: (rendererId, message) => this.postMessage(editorId, rendererId, message), + receiveMessage: async (rendererId, message) => { + this.editorWidgetService.getNotebookEditor(editorId)?.postRendererMessage(rendererId, message); + return true; + }, dispose: () => this.scopedMessaging.delete(editorId), }; @@ -93,7 +101,7 @@ export class NotebookRendererMessagingService implements Disposable { return messaging; } - private postMessage(editorId: string, rendererId: string, message: unknown): void { + protected postMessage(editorId: string, rendererId: string, message: unknown): void { if (!this.activations.has(rendererId)) { this.prepare(rendererId); } diff --git a/packages/notebook/src/browser/service/notebook-service.ts b/packages/notebook/src/browser/service/notebook-service.ts index da6e1920e4ba2..6f9697e18e67b 100644 --- a/packages/notebook/src/browser/service/notebook-service.ts +++ b/packages/notebook/src/browser/service/notebook-service.ts @@ -14,15 +14,16 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Disposable, DisposableCollection, Emitter, URI } from '@theia/core'; +import { Disposable, DisposableCollection, Emitter, Resource, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; -import { NotebookData, TransientOptions } from '../../common'; +import { CellKind, NotebookData, TransientOptions } from '../../common'; import { NotebookModel, NotebookModelFactory, NotebookModelProps } from '../view-model/notebook-model'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; -import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; import { NotebookCellModel, NotebookCellModelFactory, NotebookCellModelProps } from '../view-model/notebook-cell-model'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { NotebookMonacoTextModelService } from './notebook-monaco-text-model-service'; +import { CellEditOperation } from '../notebook-types'; export const NotebookProvider = Symbol('notebook provider'); @@ -37,21 +38,28 @@ export interface NotebookSerializer { fromNotebook(data: NotebookData): Promise; } +export interface NotebookWorkspaceEdit { + edits: { + resource: URI; + edit: CellEditOperation + }[] +} + @injectable() export class NotebookService implements Disposable { @inject(FileService) protected fileService: FileService; - @inject(MonacoTextModelService) - protected modelService: MonacoTextModelService; - @inject(NotebookModelFactory) protected notebookModelFactory: (props: NotebookModelProps) => NotebookModel; @inject(NotebookCellModelFactory) protected notebookCellModelFactory: (props: NotebookCellModelProps) => NotebookCellModel; + @inject(NotebookMonacoTextModelService) + protected textModelService: NotebookMonacoTextModelService; + protected willUseNotebookSerializerEmitter = new Emitter(); readonly onWillUseNotebookSerializer = this.willUseNotebookSerializerEmitter.event; @@ -101,29 +109,28 @@ export class NotebookService implements Disposable { }); } - async createNotebookModel(data: NotebookData, viewType: string, uri: URI): Promise { - const serializer = this.notebookProviders.get(viewType)?.serializer; - if (!serializer) { - throw new Error('no notebook serializer for ' + viewType); - } - - const model = this.notebookModelFactory({ data, uri, viewType, serializer }); - this.notebookModels.set(uri.toString(), model); + async createNotebookModel(data: NotebookData, viewType: string, resource: Resource): Promise { + const dataProvider = await this.getNotebookDataProvider(viewType); + const serializer = dataProvider.serializer; + const model = this.notebookModelFactory({ data, resource, viewType, serializer }); + this.notebookModels.set(resource.uri.toString(), model); // Resolve cell text models right after creating the notebook model // This ensures that all text models are available in the plugin host - await Promise.all(model.cells.map(e => e.resolveTextModel())); + await this.textModelService.createTextModelsForNotebook(model); this.didAddNotebookDocumentEmitter.fire(model); + model.onDidDispose(() => { + this.notebookModels.delete(resource.uri.toString()); + this.didRemoveNotebookDocumentEmitter.fire(model); + }); return model; } async getNotebookDataProvider(viewType: string): Promise { - await this.ready.promise; - - const result = await this.waitForNotebookProvider(viewType); - if (!result) { + try { + return await this.waitForNotebookProvider(viewType); + } catch { throw new Error(`No provider registered for view type: '${viewType}'`); } - return result; } /** @@ -131,11 +138,12 @@ export class NotebookService implements Disposable { * It takes a few seconds for the plugin host to start so that notebook data providers can be registered. * This methods waits until the notebook provider is registered. */ - protected async waitForNotebookProvider(type: string): Promise { - if (this.notebookProviders.has(type)) { - return this.notebookProviders.get(type); + protected waitForNotebookProvider(type: string): Promise { + const existing = this.notebookProviders.get(type); + if (existing) { + return Promise.resolve(existing); } - const deferred = new Deferred(); + const deferred = new Deferred(); // 20 seconds of timeout const timeoutDuration = 20_000; @@ -149,7 +157,12 @@ export class NotebookService implements Disposable { if (viewType === type) { clearTimeout(timeout); disposable.dispose(); - deferred.resolve(this.notebookProviders.get(type)); + const newProvider = this.notebookProviders.get(type); + if (!newProvider) { + deferred.reject(new Error(`Notebook provider for type ${type} is invalid`)); + } else { + deferred.resolve(newProvider); + } } }); timeout = setTimeout(() => { @@ -158,7 +171,9 @@ export class NotebookService implements Disposable { deferred.reject(new Error(`Timed out while waiting for notebook serializer for type ${type} to be registered`)); }, timeoutDuration); - await Promise.all(this.willUseNotebookSerializerEmitter.fire(type)); + this.ready.promise.then(() => { + this.willUseNotebookSerializerEmitter.fire(type); + }); return deferred.promise; } @@ -178,4 +193,23 @@ export class NotebookService implements Disposable { listNotebookDocuments(): NotebookModel[] { return [...this.notebookModels.values()]; } + + applyWorkspaceEdit(workspaceEdit: NotebookWorkspaceEdit): boolean { + try { + workspaceEdit.edits.forEach(edit => { + const notebook = this.getNotebookEditorModel(edit.resource); + notebook?.applyEdits([edit.edit], true); + }); + return true; + } catch (e) { + console.error(e); + return false; + } + } + + getCodeCellLanguage(model: NotebookModel): string { + const firstCodeCell = model.cells.find(cellModel => cellModel.cellKind === CellKind.Code); + const cellLanguage = firstCodeCell?.language ?? 'plaintext'; + return cellLanguage; + } } diff --git a/packages/notebook/src/browser/style/index.css b/packages/notebook/src/browser/style/index.css index 2c9b1844a0641..0ad64eb41136f 100644 --- a/packages/notebook/src/browser/style/index.css +++ b/packages/notebook/src/browser/style/index.css @@ -16,21 +16,43 @@ :root { --theia-notebook-markdown-size: 17px; + --theia-notebook-cell-editor-margin-right: 10px; } .theia-notebook-cell-list { + position: absolute; + top: 0; + width: 100%; overflow-y: auto; list-style: none; padding-left: 0px; background-color: var(--theia-notebook-editorBackground); + z-index: 0; + pointer-events: none; +} + + +.theia-notebook-cell-output-webview { + padding: 5px 0px; + margin: 0px 15px 0px 50px; + width: calc(100% - 60px); + position: absolute; + z-index: 0; } .theia-notebook-cell { - cursor: grab; display: flex; margin: 10px 0px; } +.theia-notebook-cell:focus { + outline: none; +} + +.theia-notebook-cell.draggable { + cursor: grab; +} + .theia-notebook-cell:hover .theia-notebook-cell-marker { visibility: visible; } @@ -58,22 +80,44 @@ width: calc(100% - 15px); } +/* Rendered Markdown Content */ + .theia-notebook-markdown-content { - padding: 8px 16px 8px 36px; + pointer-events: all; + padding: 8px 16px 8px 0px; font-size: var(--theia-notebook-markdown-size); } -.theia-notebook-markdown-content > *:first-child { +.theia-notebook-markdown-content>* { + font-weight: 400; +} + +.theia-notebook-markdown-content>*:first-child { margin-top: 0; padding-top: 0; } -.theia-notebook-markdown-content > *:only-child, -.theia-notebook-markdown-content > *:last-child { +.theia-notebook-markdown-content>*:last-child { margin-bottom: 0; padding-bottom: 0; } +.theia-notebook-markdown-sidebar { + width: 35px; +} + +/* Markdown cell edit mode */ +.theia-notebook-cell-content:has(.theia-notebook-markdown-editor-container>.theia-notebook-cell-editor) { + pointer-events: all; + margin-right: var(--theia-notebook-cell-editor-margin-right); + outline: 1px solid var(--theia-notebook-cellBorderColor); +} + +/* Markdown cell edit mode focused */ +.theia-notebook-cell.focused .theia-notebook-cell-content:has(.theia-notebook-markdown-editor-container>.theia-notebook-cell-editor) { + outline-color: var(--theia-notebook-focusedEditorBorder); +} + .theia-notebook-empty-markdown { opacity: 0.6; } @@ -83,13 +127,15 @@ } .theia-notebook-cell-editor-container { + pointer-events: all; width: calc(100% - 46px); flex: 1; outline: 1px solid var(--theia-notebook-cellBorderColor); - margin: 0px 10px; + margin: 0px 16px 0px 10px; } -.theia-notebook-cell.focused .theia-notebook-cell-editor-container { +/* Only mark an editor cell focused if the editor has focus */ +.theia-notebook-cell-editor-container:has(.monaco-editor.focused) { outline-color: var(--theia-notebook-focusedEditorBorder); } @@ -106,8 +152,13 @@ flex-grow: 1; } -.notebook-cell-status-right { - margin: 0 5px; +.notebook-cell-language-label { + padding: 0 5px; +} + +.notebook-cell-language-label:hover { + cursor: pointer; + background-color: var(--theia-toolbar-hoverBackground); } .notebook-cell-status-item { @@ -118,6 +169,7 @@ } .theia-notebook-cell-toolbar { + pointer-events: all; border: 1px solid var(--theia-notebook-cellToolbarSeparator); display: flex; position: absolute; @@ -126,11 +178,31 @@ background-color: var(--theia-editor-background); } -.theia-notebook-cell-sidebar { +.theia-notebook-cell-sidebar-toolbar { display: flex; flex-direction: column; padding: 2px; - background-color: var(--theia-editor-background); + flex-grow: 1; +} + +.theia-notebook-cell-sidebar { + pointer-events: all; + display: flex; +} + +.theia-notebook-cell-sidebar-actions { + display: flex; + flex-direction: column; +} + +.theia-notebook-code-cell-execution-order { + display: block; + font-family: var(--monaco-monospace-font); + font-size: 10px; + opacity: 0.7; + text-align: center; + white-space: pre; + padding: 5px 0; } .theia-notebook-cell-toolbar-item { @@ -147,7 +219,8 @@ } .theia-notebook-cell-divider { - height: 20px; + pointer-events: all; + height: 25px; width: 100%; } @@ -156,14 +229,37 @@ flex-direction: row; } -.theia-notebook-cell-sidebar { +.theia-notebook-main-container { display: flex; flex-direction: column; + height: 100%; + overflow: hidden; +} + +.theia-notebook-main-container:focus { + outline: none; +} + +.theia-notebook-main-container .theia-notebook-main-loading-indicator { + /* `progress-animation` is defined in `packages/core/src/browser/style/progress-bar.css` */ + animation: progress-animation 1.8s 0s infinite cubic-bezier(0.645, 0.045, 0.355, 1); + background-color: var(--theia-progressBar-background); + height: 2px; +} + +.theia-notebook-viewport { + display: flex; + overflow: hidden; + height: 100%; +} + +.theia-notebook-scroll-container { + flex: 1; + overflow: hidden; + position: relative; } .theia-notebook-main-toolbar { - position: sticky; - top: 0; background: var(--theia-editor-background); display: flex; flex-direction: row; @@ -182,12 +278,14 @@ cursor: pointer; } -.theia-notebook-main-toolbar-item:hover { - background-color: var(--theia-toolbar-hoverBackground); +.theia-notebook-main-toolbar-item.theia-mod-disabled:hover { + background-color: transparent; + cursor: default; } .theia-notebook-main-toolbar-item-text { padding: 0 4px; + white-space: nowrap; } .theia-notebook-toolbar-separator { @@ -205,10 +303,10 @@ border: 1px solid var(--theia-notebook-cellToolbarSeparator); background-color: var(--theia-editor-background); color: var(--theia-foreground); - vertical-align: middle; - text-align: center; + display: flex; height: 24px; margin: 0 8px; + padding: 2px 4px; } .theia-notebook-add-cell-button:hover { @@ -219,14 +317,16 @@ background-color: var(--theia-toolbar-active); } -.theia-notebook-add-cell-button-icon { +.theia-notebook-add-cell-button>* { vertical-align: middle; } -.theia-notebook-cell-output-webview { - padding: 5px 0px; - margin: 0px 10px; - width: 100%; +.theia-notebook-add-cell-button-icon::before { + font: normal normal normal 14px/1 codicon; +} + +.theia-notebook-add-cell-button-text { + margin: 1px 0 0 4px; } .theia-notebook-cell-drop-indicator { @@ -234,3 +334,196 @@ background-color: var(--theia-notebook-focusedCellBorder); width: 100%; } + +.theia-notebook-collapsed-output-container { + width: 0; + overflow: visible; +} + +.theia-notebook-collapsed-output { + text-wrap: nowrap; + padding: 4px 8px; + color: var(--theia-foreground); + margin-left: 30px; + font-size: 14px; + line-height: 22px; + opacity: 0.7; +} + +.theia-notebook-drag-ghost-image { + position: absolute; + top: -99999px; + left: -99999px; + max-height: 500px; + min-height: 100px; + background-color: var(--theia-editor-background); +} + +/* Notebook Find Widget */ + +.theia-notebook-overlay { + position: absolute; + z-index: 100; + right: 18px; +} + +.theia-notebook-find-widget { + /* position: absolute; + z-index: 35; + height: 33px; + overflow: hidden; */ + line-height: 19px; + transition: transform 200ms linear; + display: flex; + flex-direction: row; + padding: 0 4px; + box-sizing: border-box; + box-shadow: 0 0 8px 2px var(--theia-widget-shadow); + background-color: var(--theia-editorWidget-background); + color: var(--theia-editorWidget-foreground); + border-left: 1px solid var(--theia-widget-border); + border-right: 1px solid var(--theia-widget-border); + border-bottom: 1px solid var(--theia-widget-border); + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.theia-notebook-find-widget.hidden { + display: none; + transform: translateY(calc(-100% - 10px)); +} + +.theia-notebook-find-widget.search-mode>*>*:nth-child(2) { + display: none; +} + +.theia-notebook-find-widget-expand { + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + border-radius: 0; + margin-right: 4px; +} + +.theia-notebook-find-widget-expand:focus { + outline: 1px solid var(--theia-focusBorder); +} + +.theia-notebook-find-widget-expand:hover { + background-color: var(--theia-toolbar-hoverBackground); +} + +.theia-notebook-find-widget-buttons-first { + margin-bottom: 4px; + height: 26px; + display: flex; + flex-direction: row; + align-items: center; +} + +.theia-notebook-find-widget-buttons-first>div, +.theia-notebook-find-widget-buttons-second>div { + margin-right: 4px; +} + +.theia-notebook-find-widget-buttons-second { + height: 26px; + display: flex; + flex-direction: row; + align-items: center; +} + +.theia-notebook-find-widget-inputs { + margin-top: 4px; + display: flex; + flex-direction: column; +} + +.theia-notebook-find-widget-buttons { + margin-top: 4px; + margin-left: 4px; + display: flex; + flex-direction: column; +} + +.theia-notebook-find-widget-matches-count { + width: 72px; + box-sizing: border-box; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.theia-notebook-find-widget-input-wrapper { + display: flex; + align-items: center; + background: var(--theia-input-background); + border-style: solid; + border-width: var(--theia-border-width); + border-color: var(--theia-input-background); + border-radius: 2px; + margin-bottom: 4px; +} + +.theia-notebook-find-widget-input-wrapper:focus-within { + border-color: var(--theia-focusBorder); +} + +.theia-notebook-find-widget-input-wrapper .option.enabled { + color: var(--theia-inputOption-activeForeground); + outline: 1px solid var(--theia-inputOption-activeBorder); + background-color: var(--theia-inputOption-activeBackground); +} + +.theia-notebook-find-widget-input-wrapper .option { + margin: 2px; +} + +.theia-notebook-find-widget-input-wrapper .theia-notebook-find-widget-input:focus { + border: none; + outline: none; +} + +.theia-notebook-find-widget-input-wrapper .theia-notebook-find-widget-input { + background: none; + border: none; +} + +.theia-notebook-find-widget-replace { + margin-bottom: 4px; +} + +.theia-notebook-find-widget-buttons .disabled { + opacity: 0.5; +} + +mark.theia-find-match { + color: var(--theia-editor-findMatchHighlightForeground); + background-color: var(--theia-editor-findMatchHighlightBackground); +} + +mark.theia-find-match.theia-find-match-selected { + color: var(--theia-editor-findMatchForeground); + background-color: var(--theia-editor-findMatchBackground); +} + +.cell-status-bar-item { + align-items: center; + display: flex; + height: 16px; + margin: 0 3px; + overflow: hidden; + padding: 0 3px; + text-overflow: clip; + white-space: pre; +} + +.cell-status-item-has-command { + cursor: pointer; +} + +.cell-status-item-has-command:hover { + background-color: var(--theia-toolbar-hoverBackground); +} diff --git a/packages/notebook/src/browser/view-model/notebook-cell-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-model.ts index bd804be68afbd..4160bb495d74c 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -21,33 +21,57 @@ import { Disposable, DisposableCollection, Emitter, Event, URI } from '@theia/core'; import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; -import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; +import { type MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { - CellInternalMetadataChangedEvent, CellKind, NotebookCellCollapseState, NotebookCellInternalMetadata, - NotebookCellMetadata, NotebookCellOutputsSplice, CellOutput, CellData, NotebookCell, CellOutputItem + CellKind, NotebookCellCollapseState, NotebookCellInternalMetadata, + NotebookCellMetadata, CellOutput, CellData, CellOutputItem } from '../../common'; +import { NotebookCellOutputsSplice } from '../notebook-types'; +import { NotebookMonacoTextModelService } from '../service/notebook-monaco-text-model-service'; import { NotebookCellOutputModel } from './notebook-cell-output-model'; +import { PreferenceService } from '@theia/core/lib/browser'; +import { NotebookPreferences } from '../contributions/notebook-preferences'; +import { LanguageService } from '@theia/core/lib/browser/language-service'; +import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from '../view/notebook-find-widget'; +import { Range } from '@theia/core/shared/vscode-languageserver-protocol'; export const NotebookCellModelFactory = Symbol('NotebookModelFactory'); export type NotebookCellModelFactory = (props: NotebookCellModelProps) => NotebookCellModel; -export function createNotebookCellModelContainer(parent: interfaces.Container, props: NotebookCellModelProps, - notebookCellContextManager: new (...args: never[]) => unknown): interfaces.Container { +export type CellEditorFocusRequest = number | 'lastLine' | undefined; + +export function createNotebookCellModelContainer(parent: interfaces.Container, props: NotebookCellModelProps): interfaces.Container { const child = parent.createChild(); child.bind(NotebookCellModelProps).toConstantValue(props); - // We need the constructor as property here to avoid circular dependencies for the context manager - child.bind(NotebookCellContextManager).to(notebookCellContextManager).inSingletonScope(); child.bind(NotebookCellModel).toSelf(); return child; } -const NotebookCellContextManager = Symbol('NotebookCellContextManager'); -interface NotebookCellContextManager { - updateCellContext(cell: NotebookCellModel, context: HTMLElement): void; - dispose(): void; - onDidChangeContext: Event; +export interface CellInternalMetadataChangedEvent { + readonly lastRunSuccessChanged?: boolean; +} + +export interface NotebookCell { + readonly uri: URI; + handle: number; + language: string; + cellKind: CellKind; + outputs: CellOutput[]; + metadata: NotebookCellMetadata; + internalMetadata: NotebookCellInternalMetadata; + text: string; + /** + * The selection of the cell. Zero-based line/character coordinates. + */ + selection: Range | undefined; + onDidChangeOutputs?: Event; + onDidChangeOutputItems?: Event; + onDidChangeLanguage: Event; + onDidChangeMetadata: Event; + onDidChangeInternalMetadata: Event; + } const NotebookCellModelProps = Symbol('NotebookModelProps'); @@ -68,33 +92,61 @@ export interface NotebookCellModelProps { export class NotebookCellModel implements NotebookCell, Disposable { protected readonly onDidChangeOutputsEmitter = new Emitter(); - readonly onDidChangeOutputs: Event = this.onDidChangeOutputsEmitter.event; + readonly onDidChangeOutputs = this.onDidChangeOutputsEmitter.event; protected readonly onDidChangeOutputItemsEmitter = new Emitter(); - readonly onDidChangeOutputItems: Event = this.onDidChangeOutputItemsEmitter.event; + readonly onDidChangeOutputItems = this.onDidChangeOutputItemsEmitter.event; protected readonly onDidChangeContentEmitter = new Emitter<'content' | 'language' | 'mime'>(); - readonly onDidChangeContent: Event<'content' | 'language' | 'mime'> = this.onDidChangeContentEmitter.event; + readonly onDidChangeContent = this.onDidChangeContentEmitter.event; protected readonly onDidChangeMetadataEmitter = new Emitter(); - readonly onDidChangeMetadata: Event = this.onDidChangeMetadataEmitter.event; + readonly onDidChangeMetadata = this.onDidChangeMetadataEmitter.event; protected readonly onDidChangeInternalMetadataEmitter = new Emitter(); - readonly onDidChangeInternalMetadata: Event = this.onDidChangeInternalMetadataEmitter.event; + readonly onDidChangeInternalMetadata = this.onDidChangeInternalMetadataEmitter.event; protected readonly onDidChangeLanguageEmitter = new Emitter(); - readonly onDidChangeLanguage: Event = this.onDidChangeLanguageEmitter.event; + readonly onDidChangeLanguage = this.onDidChangeLanguageEmitter.event; protected readonly onDidRequestCellEditChangeEmitter = new Emitter(); readonly onDidRequestCellEditChange = this.onDidRequestCellEditChangeEmitter.event; - @inject(NotebookCellContextManager) - readonly notebookCellContextManager: NotebookCellContextManager; + protected readonly onWillFocusCellEditorEmitter = new Emitter(); + readonly onWillFocusCellEditor = this.onWillFocusCellEditorEmitter.event; + + protected readonly onWillBlurCellEditorEmitter = new Emitter(); + readonly onWillBlurCellEditor = this.onWillBlurCellEditorEmitter.event; + + protected readonly onDidChangeEditorOptionsEmitter = new Emitter(); + readonly onDidChangeEditorOptions = this.onDidChangeEditorOptionsEmitter.event; + + protected readonly outputVisibilityChangeEmitter = new Emitter(); + readonly onDidChangeOutputVisibility = this.outputVisibilityChangeEmitter.event; + + protected readonly onDidFindMatchesEmitter = new Emitter(); + readonly onDidFindMatches: Event = this.onDidFindMatchesEmitter.event; + + protected readonly onDidSelectFindMatchEmitter = new Emitter(); + readonly onDidSelectFindMatch: Event = this.onDidSelectFindMatchEmitter.event; + + protected onDidRequestCenterEditorEmitter = new Emitter(); + readonly onDidRequestCenterEditor = this.onDidRequestCenterEditorEmitter.event; + + protected onDidCellHeightChangeEmitter = new Emitter(); + readonly onDidCellHeightChange = this.onDidCellHeightChangeEmitter.event; @inject(NotebookCellModelProps) protected readonly props: NotebookCellModelProps; - @inject(MonacoTextModelService) - protected readonly textModelService: MonacoTextModelService; + + @inject(NotebookMonacoTextModelService) + protected readonly textModelService: NotebookMonacoTextModelService; + + @inject(LanguageService) + protected readonly languageService: LanguageService; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; get outputs(): NotebookCellOutputModel[] { return this._outputs; @@ -106,6 +158,11 @@ export class NotebookCellModel implements NotebookCell, Disposable { return this._metadata; } + set metadata(newMetadata: NotebookCellMetadata) { + this._metadata = newMetadata; + this.onDidChangeMetadataEmitter.fire(); + } + protected _metadata: NotebookCellMetadata; protected toDispose = new DisposableCollection(); @@ -127,24 +184,21 @@ export class NotebookCellModel implements NotebookCell, Disposable { } - textModel: MonacoEditorModel; - - protected htmlContext: HTMLLIElement; - - get context(): HTMLLIElement { - return this.htmlContext; - } + protected textModel?: MonacoEditorModel; get text(): string { - return this.textModel ? this.textModel.getText() : this.source; + return this.textModel && !this.textModel.isDisposed() ? this.textModel.getText() : this.source; } get source(): string { return this.props.source; } + set source(source: string) { this.props.source = source; + this.textModel?.textEditorModel.setValue(source); } + get language(): string { return this.props.language; } @@ -154,16 +208,19 @@ export class NotebookCellModel implements NotebookCell, Disposable { return; } - this.props.language = newLanguage; if (this.textModel) { this.textModel.setLanguageId(newLanguage); } - this.language = newLanguage; + this.props.language = newLanguage; this.onDidChangeLanguageEmitter.fire(newLanguage); this.onDidChangeContentEmitter.fire('language'); } + get languageName(): string { + return this.languageService.getLanguage(this.language)?.name ?? this.language; + } + get uri(): URI { return this.props.uri; } @@ -174,18 +231,72 @@ export class NotebookCellModel implements NotebookCell, Disposable { return this.props.cellKind; } + protected _editing: boolean = false; + get editing(): boolean { + return this._editing; + } + + protected _editorOptions: MonacoEditor.IOptions = {}; + get editorOptions(): Readonly { + return this._editorOptions; + } + + set editorOptions(options: MonacoEditor.IOptions) { + this._editorOptions = options; + this.onDidChangeEditorOptionsEmitter.fire(options); + } + + protected _outputVisible: boolean = true; + get outputVisible(): boolean { + return this._outputVisible; + } + + set outputVisible(visible: boolean) { + if (this._outputVisible !== visible) { + this._outputVisible = visible; + this.outputVisibilityChangeEmitter.fire(visible); + } + } + + protected _selection: Range | undefined = undefined; + + get selection(): Range | undefined { + return this._selection; + } + + set selection(selection: Range | undefined) { + this._selection = selection; + } + + protected _cellheight: number = 0; + get cellHeight(): number { + return this._cellheight; + } + + set cellHeight(height: number) { + if (height !== this._cellheight) { + this.onDidCellHeightChangeEmitter.fire(height); + this._cellheight = height; + } + } + @postConstruct() protected init(): void { this._outputs = this.props.outputs.map(op => new NotebookCellOutputModel(op)); this._metadata = this.props.metadata ?? {}; this._internalMetadata = this.props.internalMetadata ?? {}; - } - refChanged(node: HTMLLIElement): void { - if (node) { - this.htmlContext = node; - this.notebookCellContextManager.updateCellContext(this, node); - } + this.editorOptions = { + lineNumbers: this.preferenceService.get(NotebookPreferences.NOTEBOOK_LINE_NUMBERS) + }; + this.toDispose.push(this.preferenceService.onPreferenceChanged(e => { + if (e.preferenceName === NotebookPreferences.NOTEBOOK_LINE_NUMBERS) { + this.editorOptions = { + ...this.editorOptions, + lineNumbers: this.preferenceService.get(NotebookPreferences.NOTEBOOK_LINE_NUMBERS) + }; + } + })); } dispose(): void { @@ -195,19 +306,35 @@ export class NotebookCellModel implements NotebookCell, Disposable { this.onDidChangeMetadataEmitter.dispose(); this.onDidChangeInternalMetadataEmitter.dispose(); this.onDidChangeLanguageEmitter.dispose(); - this.notebookCellContextManager.dispose(); - this.textModel.dispose(); this.toDispose.dispose(); } requestEdit(): void { - this.onDidRequestCellEditChangeEmitter.fire(true); + if (!this.textModel || !this.textModel.readOnly) { + this._editing = true; + this.onDidRequestCellEditChangeEmitter.fire(true); + } } requestStopEdit(): void { + this._editing = false; this.onDidRequestCellEditChangeEmitter.fire(false); } + requestFocusEditor(focusRequest?: CellEditorFocusRequest): void { + this.requestEdit(); + this.onWillFocusCellEditorEmitter.fire(focusRequest); + } + + requestBlurEditor(): void { + this.requestStopEdit(); + this.onWillBlurCellEditorEmitter.fire(); + } + + requestCenterEditor(): void { + this.onDidRequestCenterEditorEmitter.fire(); + } + spliceNotebookCellOutputs(splice: NotebookCellOutputsSplice): void { if (splice.deleteCount > 0 && splice.newOutputs.length > 0) { const commonLen = Math.min(splice.deleteCount, splice.newOutputs.length); @@ -272,10 +399,97 @@ export class NotebookCellModel implements NotebookCell, Disposable { return this.textModel; } - const ref = await this.textModelService.createModelReference(this.uri); + const ref = await this.textModelService.getOrCreateNotebookCellModelReference(this.uri); this.textModel = ref.object; + this.toDispose.push(ref); + this.toDispose.push(this.textModel.onDidChangeContent(e => { + this.props.source = e.model.getText(); + })); return ref.object; } + + restartOutputRenderer(outputId: string): void { + const output = this.outputs.find(out => out.outputId === outputId); + if (output) { + this.onDidChangeOutputItemsEmitter.fire(output); + } + } + + onMarkdownFind: ((options: NotebookEditorFindMatchOptions) => NotebookEditorFindMatch[]) | undefined; + + showMatch(selected: NotebookCodeEditorFindMatch): void { + this.onDidSelectFindMatchEmitter.fire(selected); + } + + findMatches(options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[] { + if (this.cellKind === CellKind.Markup && !this.editing) { + return this.onMarkdownFind?.(options) ?? []; + } + if (!this.textModel) { + return []; + } + const matches = options.search ? this.textModel.findMatches({ + searchString: options.search, + isRegex: options.regex, + matchCase: options.matchCase, + matchWholeWord: options.wholeWord + }) : []; + const editorFindMatches = matches.map(match => new NotebookCodeEditorFindMatch(this, match.range, this.textModel!)); + this.onDidFindMatchesEmitter.fire(editorFindMatches); + return editorFindMatches; + } + + replaceAll(matches: NotebookCodeEditorFindMatch[], value: string): void { + const editOperations = matches.map(match => ({ + range: { + startColumn: match.range.start.character, + startLineNumber: match.range.start.line, + endColumn: match.range.end.character, + endLineNumber: match.range.end.line + }, + text: value + })); + this.textModel?.textEditorModel.pushEditOperations( + // eslint-disable-next-line no-null/no-null + null, + editOperations, + // eslint-disable-next-line no-null/no-null + () => null); + } +} + +export interface NotebookCellFindMatches { + matches: NotebookEditorFindMatch[]; + selected: NotebookEditorFindMatch; +} + +export class NotebookCodeEditorFindMatch implements NotebookEditorFindMatch { + + selected = false; + + constructor(readonly cell: NotebookCellModel, readonly range: Range, readonly textModel: MonacoEditorModel) { + } + + show(): void { + this.cell.showMatch(this); + } + replace(value: string): void { + this.textModel.textEditorModel.pushEditOperations( + // eslint-disable-next-line no-null/no-null + null, + [{ + range: { + startColumn: this.range.start.character, + startLineNumber: this.range.start.line, + endColumn: this.range.end.character, + endLineNumber: this.range.end.line + }, + text: value + }], + // eslint-disable-next-line no-null/no-null + () => null); + } + } function computeRunStartTimeAdjustment(oldMetadata: NotebookCellInternalMetadata, newMetadata: NotebookCellInternalMetadata): number | undefined { diff --git a/packages/notebook/src/browser/view-model/notebook-cell-output-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-output-model.ts index 0e4dbe936f366..525473b601b33 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-output-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-output-model.ts @@ -15,17 +15,14 @@ // ***************************************************************************** import { Disposable, Emitter } from '@theia/core'; -import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { CellOutput, CellOutputItem, isTextStreamMime } from '../../common'; +import { compressOutputItemStreams } from '../notebook-output-utils'; export class NotebookCellOutputModel implements Disposable { private didChangeDataEmitter = new Emitter(); readonly onDidChangeData = this.didChangeDataEmitter.event; - private requestOutputPresentationChangeEmitter = new Emitter(); - readonly onRequestOutputPresentationChange = this.requestOutputPresentationChangeEmitter.event; - get outputId(): string { return this.rawOutput.outputId; } @@ -38,7 +35,7 @@ export class NotebookCellOutputModel implements Disposable { return this.rawOutput.metadata; } - constructor(private rawOutput: CellOutput) { } + constructor(protected rawOutput: CellOutput) { } replaceData(rawData: CellOutput): void { this.rawOutput = rawData; @@ -54,11 +51,6 @@ export class NotebookCellOutputModel implements Disposable { dispose(): void { this.didChangeDataEmitter.dispose(); - this.requestOutputPresentationChangeEmitter.dispose(); - } - - requestOutputPresentationUpdate(): void { - this.requestOutputPresentationChangeEmitter.fire(); } getData(): CellOutput { @@ -73,10 +65,10 @@ export class NotebookCellOutputModel implements Disposable { if (this.outputs.length > 1 && this.outputs.every(item => isTextStreamMime(item.mime))) { // Look for the mimes in the items, and keep track of their order. // Merge the streams into one output item, per mime type. - const mimeOutputs = new Map(); + const mimeOutputs = new Map(); const mimeTypes: string[] = []; this.outputs.forEach(item => { - let items: BinaryBuffer[]; + let items: Uint8Array[]; if (mimeOutputs.has(item.mime)) { items = mimeOutputs.get(item.mime)!; } else { @@ -84,13 +76,14 @@ export class NotebookCellOutputModel implements Disposable { mimeOutputs.set(item.mime, items); mimeTypes.push(item.mime); } - items.push(item.data); + items.push(item.data.buffer); }); this.outputs.length = 0; mimeTypes.forEach(mime => { + const compressionResult = compressOutputItemStreams(mimeOutputs.get(mime)!); this.outputs.push({ mime, - data: BinaryBuffer.concat(mimeOutputs.get(mime)!) + data: compressionResult.data }); }); } diff --git a/packages/notebook/src/browser/view-model/notebook-model.ts b/packages/notebook/src/browser/view-model/notebook-model.ts index e8f1b897e3add..a80ce44e4e4ea 100644 --- a/packages/notebook/src/browser/view-model/notebook-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-model.ts @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright (C) 20023 Typefox and others. +// Copyright (C) 2023 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 @@ -14,22 +14,28 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Disposable, Emitter, URI } from '@theia/core'; +import { Disposable, Emitter, Event, QueueableEmitter, Resource, URI } from '@theia/core'; import { Saveable, SaveOptions } from '@theia/core/lib/browser'; import { - CellData, - CellEditOperation, CellEditType, CellUri, NotebookCellInternalMetadata, + CellData, CellEditType, CellUri, NotebookCellInternalMetadata, + NotebookCellMetadata, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData, - NotebookDocumentMetadata, NotebookModelWillAddRemoveEvent, - NotebookTextModelChangedEvent, NullablePartialNotebookCellInternalMetadata + NotebookDocumentMetadata, } from '../../common'; +import { + NotebookContentChangedEvent, NotebookModelWillAddRemoveEvent, + CellEditOperation, NullablePartialNotebookCellInternalMetadata, + NullablePartialNotebookCellMetadata +} from '../notebook-types'; import { NotebookSerializer } from '../service/notebook-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; -import { NotebookCellModel, NotebookCellModelFactory } from './notebook-cell-model'; -import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; +import { NotebookCellModel, NotebookCellModelFactory, NotebookCodeEditorFindMatch } from './notebook-cell-model'; import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; -import { NotebookKernel } from '../service/notebook-kernel-service'; import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; +import type { NotebookModelResolverService } from '../service/notebook-model-resolver-service'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; +import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from '../view/notebook-find-widget'; export const NotebookModelFactory = Symbol('NotebookModelFactory'); @@ -42,12 +48,19 @@ export function createNotebookModelContainer(parent: interfaces.Container, props return child; } +export const NotebookModelResolverServiceProxy = Symbol('NotebookModelResolverServiceProxy'); + const NotebookModelProps = Symbol('NotebookModelProps'); export interface NotebookModelProps { - data: NotebookData, - uri: URI, - viewType: string, - serializer: NotebookSerializer, + data: NotebookData; + resource: Resource; + viewType: string; + serializer: NotebookSerializer; +} + +export interface SelectedCellChangeEvent { + cell: NotebookCellModel | undefined; + scrollIntoView: boolean; } @injectable() @@ -62,9 +75,22 @@ export class NotebookModel implements Saveable, Disposable { protected readonly onDidAddOrRemoveCellEmitter = new Emitter(); readonly onDidAddOrRemoveCell = this.onDidAddOrRemoveCellEmitter.event; - protected readonly onDidChangeContentEmitter = new Emitter(); + protected readonly onDidChangeContentEmitter = new QueueableEmitter(); readonly onDidChangeContent = this.onDidChangeContentEmitter.event; + protected readonly onContentChangedEmitter = new Emitter(); + readonly onContentChanged = this.onContentChangedEmitter.event; + + protected readonly onDidChangeSelectedCellEmitter = new Emitter(); + readonly onDidChangeSelectedCell = this.onDidChangeSelectedCellEmitter.event; + + protected readonly onDidDisposeEmitter = new Emitter(); + readonly onDidDispose = this.onDidDisposeEmitter.event; + + get onDidChangeReadOnly(): Event { + return this.props.resource.onDidChangeReadOnly ?? Event.None; + } + @inject(FileService) protected readonly fileService: FileService; @@ -74,25 +100,49 @@ export class NotebookModel implements Saveable, Disposable { @inject(NotebookModelProps) protected props: NotebookModelProps; - @inject(MonacoTextModelService) - protected modelService: MonacoTextModelService; - @inject(NotebookCellModelFactory) protected cellModelFactory: NotebookCellModelFactory; - readonly autoSave: 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange'; - nextHandle: number = 0; + @inject(NotebookModelResolverServiceProxy) + protected modelResolverService: NotebookModelResolverService; - kernel?: NotebookKernel; + protected nextHandle: number = 0; + + protected _dirty = false; + + set dirty(dirty: boolean) { + const oldState = this._dirty; + this._dirty = dirty; + if (oldState !== dirty) { + this.onDirtyChangedEmitter.fire(); + } + } + + get dirty(): boolean { + return this._dirty; + } + + get readOnly(): boolean | MarkdownString { + return this.props.resource.readOnly ?? false; + } + + protected _selectedText = ''; + + set selectedText(value: string) { + this._selectedText = value; + } + + get selectedText(): string { + return this._selectedText; + } - dirty: boolean; selectedCell?: NotebookCellModel; protected dirtyCells: NotebookCellModel[] = []; cells: NotebookCellModel[]; get uri(): URI { - return this.props.uri; + return this.props.resource.uri; } get viewType(): string { @@ -106,7 +156,7 @@ export class NotebookModel implements Saveable, Disposable { this.dirty = false; this.cells = this.props.data.cells.map((cell, index) => this.cellModelFactory({ - uri: CellUri.generate(this.props.uri, index), + uri: CellUri.generate(this.props.resource.uri, index), handle: index, source: cell.source, language: cell.language, @@ -119,20 +169,8 @@ export class NotebookModel implements Saveable, Disposable { this.addCellOutputListeners(this.cells); - this.metadata = this.metadata; + this.metadata = this.props.data.metadata; - this.modelService.onDidCreate(editorModel => { - const modelUri = new URI(editorModel.uri); - if (modelUri.scheme === CellUri.scheme) { - const cellUri = CellUri.parse(modelUri); - if (cellUri && cellUri.notebook.isEqual(this.uri)) { - const cell = this.cells.find(c => c.handle === cellUri.handle); - if (cell) { - cell.textModel = editorModel; - } - } - } - }); this.nextHandle = this.cells.length; } @@ -141,38 +179,51 @@ export class NotebookModel implements Saveable, Disposable { this.onDidSaveNotebookEmitter.dispose(); this.onDidAddOrRemoveCellEmitter.dispose(); this.onDidChangeContentEmitter.dispose(); + this.onDidChangeSelectedCellEmitter.dispose(); this.cells.forEach(cell => cell.dispose()); + this.onDidDisposeEmitter.fire(); } - async save(options: SaveOptions): Promise { + async save(options?: SaveOptions): Promise { this.dirtyCells = []; this.dirty = false; - this.onDirtyChangedEmitter.fire(); - const serializedNotebook = await this.props.serializer.fromNotebook({ - cells: this.cells.map(cell => cell.getData()), - metadata: this.metadata - }); + const serializedNotebook = await this.serialize(); this.fileService.writeFile(this.uri, serializedNotebook); this.onDidSaveNotebookEmitter.fire(); } createSnapshot(): Saveable.Snapshot { - const model = this; return { - read(): string { - return JSON.stringify({ - cells: model.cells.map(cell => cell.getData()), - metadata: model.metadata - }); - } + read: () => JSON.stringify(this.getData()) }; } + serialize(): Promise { + return this.props.serializer.fromNotebook(this.getData()); + } + + async applySnapshot(snapshot: Saveable.Snapshot): Promise { + const rawData = Saveable.Snapshot.read(snapshot); + if (!rawData) { + throw new Error('could not read notebook snapshot'); + } + const data = JSON.parse(rawData) as NotebookData; + this.setData(data); + } + async revert(options?: Saveable.RevertOptions): Promise { + if (!options?.soft) { + // Load the data from the file again + try { + const data = await this.modelResolverService.resolveExistingNotebookData(this.props.resource, this.props.viewType); + this.setData(data, false); + } catch (err) { + console.error('Failed to revert notebook', err); + } + } this.dirty = false; - this.onDirtyChangedEmitter.fire(); } isDirty(): boolean { @@ -186,32 +237,54 @@ export class NotebookModel implements Saveable, Disposable { this.dirtyCells.splice(this.dirtyCells.indexOf(cell), 1); } - const oldDirtyState = this.dirty; this.dirty = this.dirtyCells.length > 0; - if (this.dirty !== oldDirtyState) { - this.onDirtyChangedEmitter.fire(); - } + // Only fire `onContentChangedEmitter` here, because `onDidChangeContentEmitter` is used for model level changes only + // However, this event indicates that the content of a cell has changed + this.onContentChangedEmitter.fire(); + } + + setData(data: NotebookData, markDirty = true): void { + // Replace all cells in the model + this.dirtyCells = []; + this.replaceCells(0, this.cells.length, data.cells, false, false); + this.metadata = data.metadata; + this.dirty = markDirty; + this.onDidChangeContentEmitter.fire(); + } + + getData(): NotebookData { + return { + cells: this.cells.map(cell => cell.getData()), + metadata: this.metadata + }; } undo(): void { - // TODO we probably need to check if a monaco editor is focused and if so, not undo - this.undoRedoService.undo(this.uri); + if (!this.readOnly) { + this.undoRedoService.undo(this.uri); + } } redo(): void { - // TODO see undo - this.undoRedoService.redo(this.uri); + if (!this.readOnly) { + this.undoRedoService.redo(this.uri); + } } - setSelectedCell(cell: NotebookCellModel): void { - this.selectedCell = cell; + setSelectedCell(cell: NotebookCellModel, scrollIntoView?: boolean): void { + if (this.selectedCell !== cell) { + this.selectedCell = cell; + this.onDidChangeSelectedCellEmitter.fire({ cell, scrollIntoView: scrollIntoView ?? true }); + } } private addCellOutputListeners(cells: NotebookCellModel[]): void { for (const cell of cells) { cell.onDidChangeOutputs(() => { this.dirty = true; - this.onDirtyChangedEmitter.fire(); + }); + cell.onDidRequestCellEditChange(() => { + this.onContentChangedEmitter.fire(); }); } } @@ -233,16 +306,19 @@ export class NotebookModel implements Saveable, Disposable { end: edit.editType === CellEditType.Replace ? edit.index + edit.count : cellIndex, originalIndex: index }; - }).filter(edit => !!edit); + }); for (const { edit, cellIndex } of editsWithDetails) { const cell = this.cells[cellIndex]; if (cell) { this.cellDirtyChanged(cell, true); } + + let scrollIntoView = true; switch (edit.editType) { case CellEditType.Replace: - this.replaceCells(edit.index, edit.count, edit.cells, computeUndoRedo); + this.replaceCells(edit.index, edit.count, edit.cells, computeUndoRedo, true); + scrollIntoView = edit.cells.length > 0; break; case CellEditType.Output: { if (edit.append) { @@ -250,16 +326,24 @@ export class NotebookModel implements Saveable, Disposable { } else { // could definitely be more efficient. See vscode __spliceNotebookCellOutputs2 // For now, just replace the whole existing output with the new output - cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: edit.outputs }); + cell.spliceNotebookCellOutputs({ start: 0, deleteCount: edit.deleteCount ?? cell.outputs.length, newOutputs: edit.outputs }); } - + this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.Output, index: cellIndex, outputs: cell.outputs, append: edit.append ?? false }); break; } case CellEditType.OutputItems: cell.changeOutputItems(edit.outputId, !!edit.append, edit.items); + this.onDidChangeContentEmitter.queue({ + kind: NotebookCellsChangeType.OutputItem, index: cellIndex, outputItems: edit.items, + outputId: edit.outputId, append: edit.append ?? false + }); + break; case CellEditType.Metadata: - this.updateNotebookMetadata(edit.metadata, computeUndoRedo); + this.changeCellMetadata(this.cells[cellIndex], edit.metadata, false); + break; + case CellEditType.PartialMetadata: + this.changeCellMetadataPartial(this.cells[cellIndex], edit.metadata, false); break; case CellEditType.PartialInternalMetadata: this.changeCellInternalMetadataPartial(this.cells[cellIndex], edit.internalMetadata); @@ -268,16 +352,28 @@ export class NotebookModel implements Saveable, Disposable { this.changeCellLanguage(this.cells[cellIndex], edit.language, computeUndoRedo); break; case CellEditType.DocumentMetadata: + this.updateNotebookMetadata(edit.metadata, false); break; case CellEditType.Move: this.moveCellToIndex(cellIndex, edit.length, edit.newIdx, computeUndoRedo); break; + } + // if selected cell is affected update it because it can potentially have been replaced + if (cell === this.selectedCell) { + this.setSelectedCell(this.cells[Math.min(cellIndex, this.cells.length - 1)], scrollIntoView); } } + + this.fireContentChange(); + } + + protected fireContentChange(): void { + this.onDidChangeContentEmitter.fire(); + this.onContentChangedEmitter.fire(); } - private replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean): void { + protected replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean, requestEdit: boolean): void { const cells = newCells.map(cell => { const handle = this.nextHandle++; return this.cellModelFactory({ @@ -294,7 +390,7 @@ export class NotebookModel implements Saveable, Disposable { }); this.addCellOutputListeners(cells); - const changes: NotebookCellTextModelSplice[] = [[start, deleteCount, cells]]; + const changes: NotebookCellTextModelSplice[] = [{ start, deleteCount, newItems: cells }]; const deletedCells = this.cells.splice(start, deleteCount, ...cells); @@ -304,15 +400,26 @@ export class NotebookModel implements Saveable, Disposable { if (computeUndoRedo) { this.undoRedoService.pushElement(this.uri, - async () => this.replaceCells(start, newCells.length, deletedCells.map(cell => cell.getData()), false), - async () => this.replaceCells(start, deleteCount, newCells, false)); + async () => { + this.replaceCells(start, newCells.length, deletedCells.map(cell => cell.getData()), false, false); + this.fireContentChange(); + }, + async () => { + this.replaceCells(start, deleteCount, newCells, false, false); + this.fireContentChange(); + } + ); } - this.onDidAddOrRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes } }); - this.onDidChangeContentEmitter.fire({ rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, changes }] }); + this.onDidAddOrRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes }, newCellIds: cells.map(cell => cell.handle) }); + this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ModelChange, changes }); + if (cells.length > 0 && requestEdit) { + this.setSelectedCell(cells[cells.length - 1]); + cells[cells.length - 1].requestEdit(); + } } - private changeCellInternalMetadataPartial(cell: NotebookCellModel, internalMetadata: NullablePartialNotebookCellInternalMetadata): void { + protected changeCellInternalMetadataPartial(cell: NotebookCellModel, internalMetadata: NullablePartialNotebookCellInternalMetadata): void { const newInternalMetadata: NotebookCellInternalMetadata = { ...cell.internalMetadata }; @@ -323,14 +430,10 @@ export class NotebookModel implements Saveable, Disposable { } cell.internalMetadata = newInternalMetadata; - this.onDidChangeContentEmitter.fire({ - rawEvents: [ - { kind: NotebookCellsChangeType.ChangeCellInternalMetadata, index: this.cells.indexOf(cell), internalMetadata: newInternalMetadata } - ] - }); + this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellInternalMetadata, index: this.cells.indexOf(cell), internalMetadata: newInternalMetadata }); } - private updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean): void { + protected updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean): void { const oldMetadata = this.metadata; if (computeUndoRedo) { this.undoRedoService.pushElement(this.uri, @@ -340,43 +443,112 @@ export class NotebookModel implements Saveable, Disposable { } this.metadata = metadata; - this.onDidChangeContentEmitter.fire({ - rawEvents: [{ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata }], - synchronous: true, - }); + this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata }); + } + + protected changeCellMetadataPartial(cell: NotebookCellModel, metadata: NullablePartialNotebookCellMetadata, computeUndoRedo: boolean): void { + const newMetadata: NotebookCellMetadata = { + ...cell.metadata + }; + let k: keyof NullablePartialNotebookCellMetadata; + // eslint-disable-next-line guard-for-in + for (k in metadata) { + const value = metadata[k] ?? undefined; + newMetadata[k] = value as unknown; + } + + this.changeCellMetadata(cell, newMetadata, computeUndoRedo); } - private changeCellLanguage(cell: NotebookCellModel, languageId: string, computeUndoRedo: boolean): void { + protected changeCellMetadata(cell: NotebookCellModel, metadata: NotebookCellMetadata, computeUndoRedo: boolean): void { + const triggerDirtyChange = this.isCellMetadataChanged(cell.metadata, metadata); + + if (triggerDirtyChange) { + if (computeUndoRedo) { + const oldMetadata = cell.metadata; + cell.metadata = metadata; + this.undoRedoService.pushElement(this.uri, + async () => { cell.metadata = oldMetadata; }, + async () => { cell.metadata = metadata; } + ); + } + } + + cell.metadata = metadata; + this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellMetadata, index: this.cells.indexOf(cell), metadata: cell.metadata }); + } + + protected changeCellLanguage(cell: NotebookCellModel, languageId: string, computeUndoRedo: boolean): void { if (cell.language === languageId) { return; } cell.language = languageId; - this.onDidChangeContentEmitter.fire({ - rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this.cells.indexOf(cell), language: languageId }], - synchronous: true, - }); + this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this.cells.indexOf(cell), language: languageId }); } - private moveCellToIndex(fromIndex: number, length: number, toIndex: number, computeUndoRedo: boolean): boolean { + protected moveCellToIndex(fromIndex: number, length: number, toIndex: number, computeUndoRedo: boolean): boolean { if (computeUndoRedo) { this.undoRedoService.pushElement(this.uri, - async () => { this.moveCellToIndex(toIndex, length, fromIndex, false); }, - async () => { this.moveCellToIndex(fromIndex, length, toIndex, false); } + async () => { + this.moveCellToIndex(toIndex, length, fromIndex, false); + this.fireContentChange(); + }, + async () => { + this.moveCellToIndex(fromIndex, length, toIndex, false); + this.fireContentChange(); + } ); } const cells = this.cells.splice(fromIndex, length); this.cells.splice(toIndex, 0, ...cells); - this.onDidChangeContentEmitter.fire({ - rawEvents: [{ kind: NotebookCellsChangeType.Move, index: fromIndex, length, newIdx: toIndex, cells }], - }); + this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.Move, index: fromIndex, length, newIdx: toIndex, cells }); return true; } - private getCellIndexByHandle(handle: number): number { + getCellIndexByHandle(handle: number): number { return this.cells.findIndex(c => c.handle === handle); } + + getCellByHandle(handle: number): NotebookCellModel | undefined { + return this.cells.find(c => c.handle === handle); + } + + protected isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata): boolean { + const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); + for (const key of keys) { + if (a[key] !== b[key]) { + return true; + } + } + + return false; + } + + findMatches(options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[] { + const matches: NotebookEditorFindMatch[] = []; + for (const cell of this.cells) { + matches.push(...cell.findMatches(options)); + } + return matches; + } + + replaceAll(matches: NotebookEditorFindMatch[], text: string): void { + const matchMap = new Map(); + for (const match of matches) { + if (match instanceof NotebookCodeEditorFindMatch) { + if (!matchMap.has(match.cell)) { + matchMap.set(match.cell, []); + } + matchMap.get(match.cell)?.push(match); + } + } + for (const [cell, cellMatches] of matchMap) { + cell.replaceAll(cellMatches, text); + } + } + } diff --git a/packages/notebook/src/browser/view/notebook-cell-editor.tsx b/packages/notebook/src/browser/view/notebook-cell-editor.tsx index a029e9b4ced1e..e03d1eb4df385 100644 --- a/packages/notebook/src/browser/view/notebook-cell-editor.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-editor.tsx @@ -16,37 +16,130 @@ import * as React from '@theia/core/shared/react'; import { NotebookModel } from '../view-model/notebook-model'; -import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { NotebookCellModel, NotebookCodeEditorFindMatch } from '../view-model/notebook-cell-model'; import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor'; -import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; +import { MonacoEditor, MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; -import { DisposableCollection } from '@theia/core'; +import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; +import { NotebookContextManager } from '../service/notebook-context-manager'; +import { DisposableCollection, OS } from '@theia/core'; +import { NotebookViewportService } from './notebook-viewport-service'; +import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; +import { NOTEBOOK_CELL_CURSOR_FIRST_LINE, NOTEBOOK_CELL_CURSOR_LAST_LINE } from '../contributions/notebook-context-keys'; +import { EditorExtensionsRegistry } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorExtensions'; +import { ModelDecorationOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel'; +import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from '@theia/monaco-editor-core/esm/vs/editor/common/model'; +import { animationFrame } from '@theia/core/lib/browser'; +import { NotebookCellEditorService } from '../service/notebook-cell-editor-service'; interface CellEditorProps { - notebookModel: NotebookModel, - cell: NotebookCellModel, - monacoServices: MonacoEditorServices + notebookModel: NotebookModel; + cell: NotebookCellModel; + monacoServices: MonacoEditorServices; + notebookContextManager: NotebookContextManager; + notebookCellEditorService: NotebookCellEditorService; + notebookViewportService?: NotebookViewportService; + fontInfo?: BareFontInfo; } -const DEFAULT_EDITOR_OPTIONS = { +const DEFAULT_EDITOR_OPTIONS: MonacoEditor.IOptions = { ...MonacoEditorProvider.inlineOptions, minHeight: -1, maxHeight: -1, scrollbar: { ...MonacoEditorProvider.inlineOptions.scrollbar, alwaysConsumeMouseWheel: false - } + }, + lineDecorationsWidth: 10, }; +export const CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + description: 'current-find-match', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + zIndex: 13, + className: 'currentFindMatch', + inlineClassName: 'currentFindMatchInline', + showIfCollapsed: true, + overviewRuler: { + color: 'editorOverviewRuler.findMatchForeground', + position: OverviewRulerLane.Center + } +}); + +export const FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + description: 'find-match', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + zIndex: 10, + className: 'findMatch', + inlineClassName: 'findMatchInline', + showIfCollapsed: true, + overviewRuler: { + color: 'editorOverviewRuler.findMatchForeground', + position: OverviewRulerLane.Center + } +}); + export class CellEditor extends React.Component { protected editor?: SimpleMonacoEditor; protected toDispose = new DisposableCollection(); protected container?: HTMLDivElement; + protected matches: NotebookCodeEditorFindMatch[] = []; + protected oldMatchDecorations: string[] = []; override componentDidMount(): void { this.disposeEditor(); - this.initEditor(); + this.toDispose.push(this.props.cell.onWillFocusCellEditor(focusRequest => { + this.editor?.getControl().focus(); + const lineCount = this.editor?.getControl().getModel()?.getLineCount(); + if (focusRequest && lineCount !== undefined) { + this.editor?.getControl().setPosition(focusRequest === 'lastLine' ? + { lineNumber: lineCount, column: 1 } : + { lineNumber: focusRequest, column: 1 }, + 'keyboard'); + } + const currentLine = this.editor?.getControl().getPosition()?.lineNumber; + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_FIRST_LINE, currentLine === 1); + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_LAST_LINE, currentLine === lineCount); + })); + + this.toDispose.push(this.props.cell.onWillBlurCellEditor(() => this.blurEditor())); + + this.toDispose.push(this.props.cell.onDidChangeEditorOptions(options => { + this.editor?.getControl().updateOptions(options); + })); + + this.toDispose.push(this.props.cell.onDidChangeLanguage(language => { + this.editor?.setLanguage(language); + })); + + this.toDispose.push(this.props.cell.onDidFindMatches(matches => { + this.matches = matches; + animationFrame().then(() => this.setMatches()); + })); + + this.toDispose.push(this.props.cell.onDidSelectFindMatch(match => this.centerEditorInView())); + + this.toDispose.push(this.props.notebookModel.onDidChangeSelectedCell(e => { + if (e.cell !== this.props.cell && this.editor?.getControl().hasTextFocus()) { + this.blurEditor(); + } + })); + if (!this.props.notebookViewportService || (this.container && this.props.notebookViewportService.isElementInViewport(this.container))) { + this.initEditor(); + } else { + const disposable = this.props.notebookViewportService?.onDidChangeViewport(() => { + if (!this.editor && this.container && this.props.notebookViewportService!.isElementInViewport(this.container)) { + this.initEditor(); + disposable.dispose(); + } + }); + this.toDispose.push(disposable); + } + + this.toDispose.push(this.props.cell.onDidRequestCenterEditor(() => { + this.centerEditorInView(); + })); } override componentWillUnmount(): void { @@ -54,21 +147,42 @@ export class CellEditor extends React.Component { } protected disposeEditor(): void { + if (this.editor) { + this.props.notebookCellEditorService.editorDisposed(this.editor.uri); + } this.toDispose.dispose(); this.toDispose = new DisposableCollection(); } + protected centerEditorInView(): void { + const editorDomNode = this.editor?.getControl().getDomNode(); + if (editorDomNode) { + editorDomNode.scrollIntoView({ + behavior: 'instant', + block: 'center' + }); + } else { + this.container?.scrollIntoView({ + behavior: 'instant', + block: 'center' + }); + } + } + protected async initEditor(): Promise { const { cell, notebookModel, monacoServices } = this.props; if (this.container) { const editorNode = this.container; + editorNode.style.height = ''; const editorModel = await cell.resolveTextModel(); const uri = cell.uri; this.editor = new SimpleMonacoEditor(uri, editorModel, editorNode, monacoServices, - DEFAULT_EDITOR_OPTIONS); + { ...DEFAULT_EDITOR_OPTIONS, ...cell.editorOptions }, + [[IContextKeyService, this.props.notebookContextManager.scopedStore]], + { contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== 'editor.contrib.findController') }); this.toDispose.push(this.editor); this.editor.setLanguage(cell.language); this.toDispose.push(this.editor.getControl().onDidContentSizeChange(() => { @@ -77,21 +191,93 @@ export class CellEditor extends React.Component { })); this.toDispose.push(this.editor.onDocumentContentChanged(e => { notebookModel.cellDirtyChanged(cell, true); - cell.source = e.document.getText(); })); + this.toDispose.push(this.editor.getControl().onDidFocusEditorText(() => { + this.props.notebookModel.setSelectedCell(cell, false); + this.props.notebookCellEditorService.editorFocusChanged(this.editor); + })); + this.toDispose.push(this.editor.getControl().onDidBlurEditorText(() => { + if (this.props.notebookCellEditorService.getActiveCell()?.uri.toString() === this.props.cell.uri.toString()) { + this.props.notebookCellEditorService.editorFocusChanged(undefined); + } + })); + + this.toDispose.push(this.editor.getControl().onDidChangeCursorSelection(e => { + const selectedText = this.editor!.getControl().getModel()!.getValueInRange(e.selection); + // TODO handle secondary selections + this.props.cell.selection = { + start: { line: e.selection.startLineNumber - 1, character: e.selection.startColumn - 1 }, + end: { line: e.selection.endLineNumber - 1, character: e.selection.endColumn - 1 } + }; + this.props.notebookModel.selectedText = selectedText; + })); + this.toDispose.push(this.editor.getControl().onDidChangeCursorPosition(e => { + if (e.secondaryPositions.length === 0) { + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_FIRST_LINE, e.position.lineNumber === 1); + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_LAST_LINE, + e.position.lineNumber === this.editor!.getControl().getModel()!.getLineCount()); + } else { + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_FIRST_LINE, false); + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_LAST_LINE, false); + } + })); + this.props.notebookCellEditorService.editorCreated(uri, this.editor); + this.setMatches(); + if (notebookModel.selectedCell === cell) { + this.editor.getControl().focus(); + } + } + } + + protected setMatches(): void { + if (!this.editor) { + return; } + const decorations: IModelDeltaDecoration[] = []; + for (const match of this.matches) { + const decoration = match.selected ? CURRENT_FIND_MATCH_DECORATION : FIND_MATCH_DECORATION; + decorations.push({ + range: { + startLineNumber: match.range.start.line, + startColumn: match.range.start.character, + endLineNumber: match.range.end.line, + endColumn: match.range.end.character + }, + options: decoration + }); + } + + this.oldMatchDecorations = this.editor.getControl() + .changeDecorations(accessor => accessor.deltaDecorations(this.oldMatchDecorations, decorations)); } - protected assignRef = (component: HTMLDivElement) => { - this.container = component; + protected setContainer(component: HTMLDivElement | null): void { + this.container = component ?? undefined; }; protected handleResize = () => { this.editor?.refresh(); }; + protected estimateHeight(): string { + const lineHeight = this.props.fontInfo?.lineHeight ?? 20; + return this.props.cell.text.split(OS.backend.EOL).length * lineHeight + 10 + 7 + 'px'; + } + override render(): React.ReactNode { - return
    ; + return
    this.setContainer(container)} style={{ height: this.editor ? undefined : this.estimateHeight() }}> +
    ; + } + + protected blurEditor(): void { + let parent = this.container?.parentElement; + while (parent && !parent.classList.contains('theia-notebook-cell')) { + parent = parent.parentElement; + } + if (parent) { + parent.focus(); + } } } diff --git a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx index ada36f51fc247..7753a74e97f3b 100644 --- a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx @@ -14,28 +14,43 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import * as React from '@theia/core/shared/react'; -import { CellEditType, CellKind } from '../../common'; +import { CellEditType, CellKind, NotebookCellsChangeType } from '../../common'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory'; -import { codicon } from '@theia/core/lib/browser'; -import { CommandRegistry, DisposableCollection, nls } from '@theia/core'; -import { NotebookCommands } from '../contributions/notebook-actions-contribution'; +import { animationFrame, onDomEvent } from '@theia/core/lib/browser'; +import { CommandRegistry, DisposableCollection, MenuModelRegistry, MenuNode, nls } from '@theia/core'; +import { NotebookCommands, NotebookMenus } from '../contributions/notebook-actions-contribution'; import { NotebookCellActionContribution } from '../contributions/notebook-cell-actions-contribution'; +import { NotebookContextManager } from '../service/notebook-context-manager'; export interface CellRenderer { render(notebookData: NotebookModel, cell: NotebookCellModel, index: number): React.ReactNode + renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode + renderDragImage(cell: NotebookCellModel): HTMLElement +} + +export function observeCellHeight(ref: HTMLDivElement | null, cell: NotebookCellModel): void { + if (ref) { + cell.cellHeight = ref?.getBoundingClientRect().height ?? 0; + new ResizeObserver(entries => + cell.cellHeight = ref?.getBoundingClientRect().height ?? 0 + ).observe(ref); + } } interface CellListProps { renderers: Map; notebookModel: NotebookModel; + notebookContext: NotebookContextManager; toolbarRenderer: NotebookCellToolbarFactory; - commandRegistry: CommandRegistry + commandRegistry: CommandRegistry; + menuRegistry: MenuModelRegistry; } interface NotebookCellListState { selectedCell?: NotebookCellModel; + scrollIntoView: boolean; dragOverIndicator: { cell: NotebookCellModel, position: 'top' | 'bottom' } | undefined; } @@ -43,11 +58,59 @@ export class NotebookCellListView extends React.Component = React.createRef(); + constructor(props: CellListProps) { super(props); - this.state = { selectedCell: undefined, dragOverIndicator: undefined }; + this.state = { selectedCell: props.notebookModel.selectedCell, dragOverIndicator: undefined, scrollIntoView: true }; this.toDispose.push(props.notebookModel.onDidAddOrRemoveCell(e => { - this.setState({ selectedCell: undefined }); + if (e.newCellIds && e.newCellIds.length > 0) { + this.setState({ + ...this.state, + selectedCell: this.props.notebookModel.cells.find(model => model.handle === e.newCellIds![e.newCellIds!.length - 1]), + scrollIntoView: true + }); + } else { + this.setState({ + ...this.state, + selectedCell: this.props.notebookModel.cells.find(cell => cell === this.state.selectedCell), + scrollIntoView: false + }); + } + })); + + this.toDispose.push(props.notebookModel.onDidChangeContent(events => { + if (events.some(e => e.kind === NotebookCellsChangeType.Move)) { + // When a cell has been moved, we need to rerender the whole component + this.forceUpdate(); + } + })); + + this.toDispose.push(props.notebookModel.onDidChangeSelectedCell(e => { + this.setState({ + ...this.state, + selectedCell: e.cell, + scrollIntoView: e.scrollIntoView + }); + })); + + this.toDispose.push(onDomEvent(document, 'focusin', () => { + animationFrame().then(() => { + if (!this.cellListRef.current) { + return; + } + let hasCellFocus = false; + let hasFocus = false; + if (this.cellListRef.current.contains(document.activeElement)) { + if (this.props.notebookModel.selectedCell) { + hasCellFocus = true; + } + hasFocus = true; + } + this.props.notebookContext.changeCellFocus(hasCellFocus); + this.props.notebookContext.changeCellListFocus(hasFocus); + }); })); } @@ -56,36 +119,61 @@ export class NotebookCellListView extends React.Component + return
      this.onDragStart(e)}> {this.props.notebookModel.cells .map((cell, index) => - this.onAddNewCell(kind, index)} + this.isEnabled()} + onAddNewCell={(commandId: string) => this.onAddNewCell(commandId, index)} onDrop={e => this.onDrop(e, index)} onDragOver={e => this.onDragOver(e, cell, 'top')} /> - {this.shouldRenderDragOverIndicator(cell, 'top') && } -
    • { - this.setState({ selectedCell: cell }); - this.props.notebookModel.setSelectedCell(cell); + +
    • { + NotebookCellListView.dragGhost?.remove(); + this.setState({ ...this.state, dragOverIndicator: undefined }); }} - onDragStart={e => this.onDragStart(e, index)} onDragOver={e => this.onDragOver(e, cell)} onDrop={e => this.onDrop(e, index)} draggable={true} - ref={(node: HTMLLIElement) => cell.refChanged(node)}> -
      + tabIndex={-1} + data-cell-handle={cell.handle} + ref={ref => { + if (ref && cell === this.state.selectedCell && this.state.scrollIntoView) { + ref.scrollIntoView({ block: 'nearest' }); + if (cell.cellKind === CellKind.Markup && !cell.editing) { + ref.focus(); + } + } + }} + onClick={e => { + this.setState({ ...this.state, selectedCell: cell }); + this.props.notebookModel.setSelectedCell(cell, false); + }} + > +
      +
      + {this.renderCellSidebar(cell)} +
      {this.renderCellContent(cell, index)}
      {this.state.selectedCell === cell && - this.props.toolbarRenderer.renderCellToolbar(NotebookCellActionContribution.ACTION_MENU, this.props.notebookModel, cell)} + this.props.toolbarRenderer.renderCellToolbar(NotebookCellActionContribution.ACTION_MENU, cell, { + contextMenuArgs: () => [cell], commandArgs: () => [this.props.notebookModel] + }) + }
    • - {this.shouldRenderDragOverIndicator(cell, 'bottom') && } +
      ) } - this.onAddNewCell(kind, this.props.notebookModel.cells.length)} + this.isEnabled()} + onAddNewCell={(commandId: string) => this.onAddNewCell(commandId, this.props.notebookModel.cells.length)} onDrop={e => this.onDrop(e, this.props.notebookModel.cells.length - 1)} onDragOver={e => this.onDragOver(e, this.props.notebookModel.cells[this.props.notebookModel.cells.length - 1], 'bottom')} />
    ; @@ -99,20 +187,60 @@ export class NotebookCellListView extends React.Component, index: number): void { + renderCellSidebar(cell: NotebookCellModel): React.ReactNode { + const renderer = this.props.renderers.get(cell.cellKind); + if (!renderer) { + throw new Error(`No renderer found for cell type ${cell.cellKind}`); + } + return renderer.renderSidebar(this.props.notebookModel, cell); + } + + protected onDragStart(event: React.DragEvent): void { event.stopPropagation(); - event.dataTransfer.setData('text/notebook-cell-index', index.toString()); + if (!this.isEnabled()) { + event.preventDefault(); + return; + } + + const cellHandle = (event.target as HTMLLIElement).getAttribute('data-cell-handle'); + + if (!cellHandle) { + throw new Error('Cell handle not found in element for cell drag event'); + } + + const index = this.props.notebookModel.getCellIndexByHandle(parseInt(cellHandle)); + const cell = this.props.notebookModel.cells[index]; + + NotebookCellListView.dragGhost = document.createElement('div'); + NotebookCellListView.dragGhost.classList.add('theia-notebook-drag-ghost-image'); + NotebookCellListView.dragGhost.appendChild(this.props.renderers.get(cell.cellKind)?.renderDragImage(cell) ?? document.createElement('div')); + document.body.appendChild(NotebookCellListView.dragGhost); + event.dataTransfer.setDragImage(NotebookCellListView.dragGhost, -10, 0); + + event.dataTransfer.setData('text/theia-notebook-cell-index', index.toString()); + event.dataTransfer.setData('text/plain', this.props.notebookModel.cells[index].source); } protected onDragOver(event: React.DragEvent, cell: NotebookCellModel, position?: 'top' | 'bottom'): void { + if (!this.isEnabled()) { + return; + } event.preventDefault(); event.stopPropagation(); // show indicator this.setState({ ...this.state, dragOverIndicator: { cell, position: position ?? event.nativeEvent.offsetY < event.currentTarget.clientHeight / 2 ? 'top' : 'bottom' } }); } + protected isEnabled(): boolean { + return !Boolean(this.props.notebookModel.readOnly); + } + protected onDrop(event: React.DragEvent, dropElementIndex: number): void { - const index = parseInt(event.dataTransfer.getData('text/notebook-cell-index')); + if (!this.isEnabled()) { + this.setState({ dragOverIndicator: undefined }); + return; + } + const index = parseInt(event.dataTransfer.getData('text/theia-notebook-cell-index')); const isTargetBelow = index < dropElementIndex; let newIdx = this.state.dragOverIndicator?.position === 'top' ? dropElementIndex : dropElementIndex + 1; newIdx = isTargetBelow ? newIdx - 1 : newIdx; @@ -127,16 +255,19 @@ export class NotebookCellListView extends React.Component void; + isVisible: () => boolean; + onAddNewCell: (commandId: string) => void; onDrop: (event: React.DragEvent) => void; onDragOver: (event: React.DragEvent) => void; + menuRegistry: MenuModelRegistry; } -export function NotebookCellDivider({ onAddNewCell, onDrop, onDragOver }: NotebookCellDividerProps): React.JSX.Element { +export function NotebookCellDivider({ isVisible, onAddNewCell, onDrop, onDragOver, menuRegistry }: NotebookCellDividerProps): React.JSX.Element { const [hover, setHover] = React.useState(false); + const menuPath = NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP; + const menuItems = menuRegistry.getMenuNode(menuPath).children; + + const renderItem = (item: MenuNode): React.ReactNode => ; + return
  • setHover(true)} onMouseLeave={() => setHover(false)} onDrop={onDrop} onDragOver={onDragOver}> - {hover &&
    - - + {hover && isVisible() &&
    + {menuItems.map((item: MenuNode) => renderItem(item))}
    }
  • ; } -function CellDropIndicator(): React.JSX.Element { - return
    ; +function CellDropIndicator(props: { visible: boolean }): React.JSX.Element { + return
    ; } diff --git a/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx b/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx index 31fb3a15d2e5c..fe9f8bd4fac35 100644 --- a/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx @@ -20,15 +20,21 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { NotebookCellSidebar, NotebookCellToolbar } from './notebook-cell-toolbar'; import { ContextMenuRenderer } from '@theia/core/lib/browser'; -import { NotebookModel } from '../view-model/notebook-model'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; -import { NotebookCellOutputModel } from '../view-model/notebook-cell-output-model'; +import { NotebookContextManager } from '../service/notebook-context-manager'; export interface NotebookCellToolbarItem { id: string; icon?: string; label?: string; onClick: (e: React.MouseEvent) => void; + isVisible: () => boolean; + contextKeys?: Set +} + +export interface toolbarItemOptions { + contextMenuArgs?: () => unknown[]; + commandArgs?: () => unknown[]; } @injectable() @@ -46,32 +52,34 @@ export class NotebookCellToolbarFactory { @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; - renderCellToolbar(menuPath: string[], notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { - return this.getMenuItems(menuPath, notebookModel, cell)} - onContextKeysChanged={cell.notebookCellContextManager.onDidChangeContext} />; + @inject(NotebookContextManager) + protected readonly notebookContextManager: NotebookContextManager; + + renderCellToolbar(menuPath: string[], cell: NotebookCellModel, itemOptions: toolbarItemOptions): React.ReactNode { + return this.getMenuItems(menuPath, cell, itemOptions)} + onContextKeysChanged={this.notebookContextManager.onDidChangeContext} />; } - renderSidebar(menuPath: string[], notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel): React.ReactNode { - return this.getMenuItems(menuPath, notebookModel, cell, output)} - onContextKeysChanged={cell.notebookCellContextManager.onDidChangeContext} />; + renderSidebar(menuPath: string[], cell: NotebookCellModel, itemOptions: toolbarItemOptions): React.ReactNode { + return this.getMenuItems(menuPath, cell, itemOptions)} + onContextKeysChanged={this.notebookContextManager.onDidChangeContext} />; } - private getMenuItems(menuItemPath: string[], notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel): NotebookCellToolbarItem[] { + private getMenuItems(menuItemPath: string[], cell: NotebookCellModel, itemOptions: toolbarItemOptions): NotebookCellToolbarItem[] { const inlineItems: NotebookCellToolbarItem[] = []; - for (const menuNode of this.menuRegistry.getMenu(menuItemPath).children) { - if (!menuNode.when || this.contextKeyService.match(menuNode.when, cell.context ?? undefined)) { + if (!menuNode.when || this.notebookContextManager.getCellContext(cell.handle).match(menuNode.when, this.notebookContextManager.context)) { if (menuNode.role === CompoundMenuNodeRole.Flat) { - inlineItems.push(...menuNode.children?.map(child => this.createToolbarItem(child, notebookModel, cell, output)) ?? []); + inlineItems.push(...menuNode.children?.map(child => this.createToolbarItem(child, itemOptions)) ?? []); } else { - inlineItems.push(this.createToolbarItem(menuNode, notebookModel, cell, output)); + inlineItems.push(this.createToolbarItem(menuNode, itemOptions)); } } } return inlineItems; } - private createToolbarItem(menuNode: MenuNode, notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel): NotebookCellToolbarItem { + private createToolbarItem(menuNode: MenuNode, itemOptions: toolbarItemOptions): NotebookCellToolbarItem { const menuPath = menuNode.role === CompoundMenuNodeRole.Submenu ? this.menuRegistry.getPath(menuNode) : undefined; return { id: menuNode.id, @@ -83,9 +91,12 @@ export class NotebookCellToolbarFactory { anchor: e.nativeEvent, menuPath, includeAnchorArg: false, - args: [notebookModel, cell, output] + args: itemOptions.contextMenuArgs?.(), + context: this.notebookContextManager.context }) : - () => this.commandRegistry.executeCommand(menuNode.command!, notebookModel, cell, output) + () => this.commandRegistry.executeCommand(menuNode.command!, ...(itemOptions.commandArgs?.() ?? [])), + isVisible: () => menuPath ? true : Boolean(this.commandRegistry.getVisibleHandler(menuNode.command!, ...(itemOptions.commandArgs?.() ?? []))), + contextKeys: menuNode.when ? this.contextKeyService.parseKeys(menuNode.when) : undefined }; } } diff --git a/packages/notebook/src/browser/view/notebook-cell-toolbar.tsx b/packages/notebook/src/browser/view/notebook-cell-toolbar.tsx index d1e366eb43b1b..426878d51005b 100644 --- a/packages/notebook/src/browser/view/notebook-cell-toolbar.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-toolbar.tsx @@ -17,24 +17,28 @@ import * as React from '@theia/core/shared/react'; import { ACTION_ITEM } from '@theia/core/lib/browser'; import { NotebookCellToolbarItem } from './notebook-cell-toolbar-factory'; import { DisposableCollection, Event } from '@theia/core'; +import { ContextKeyChangeEvent } from '@theia/core/lib/browser/context-key-service'; export interface NotebookCellToolbarProps { getMenuItems: () => NotebookCellToolbarItem[]; - onContextKeysChanged: Event; + onContextKeysChanged: Event; } interface NotebookCellToolbarState { inlineItems: NotebookCellToolbarItem[]; } -abstract class NotebookCellActionItems extends React.Component { +abstract class NotebookCellActionBar extends React.Component { protected toDispose = new DisposableCollection(); constructor(props: NotebookCellToolbarProps) { super(props); this.toDispose.push(props.onContextKeysChanged(e => { - this.setState({ inlineItems: this.props.getMenuItems() }); + const menuItems = this.props.getMenuItems(); + if (menuItems.some(item => item.contextKeys ? e.affects(item.contextKeys) : false)) { + this.setState({ inlineItems: menuItems }); + } })); this.state = { inlineItems: this.props.getMenuItems() }; } @@ -44,26 +48,26 @@ abstract class NotebookCellActionItems extends React.Component; + return
    ; } } -export class NotebookCellToolbar extends NotebookCellActionItems { +export class NotebookCellToolbar extends NotebookCellActionBar { override render(): React.ReactNode { return
    - {this.state.inlineItems.map(item => this.renderItem(item))} + {this.state.inlineItems.filter(e => e.isVisible()).map(item => this.renderItem(item))}
    ; } } -export class NotebookCellSidebar extends NotebookCellActionItems { +export class NotebookCellSidebar extends NotebookCellActionBar { override render(): React.ReactNode { - return
    - {this.state.inlineItems.map(item => this.renderItem(item))} + return
    + {this.state.inlineItems.filter(e => e.isVisible()).map(item => this.renderItem(item))}
    ; } } diff --git a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx index 4ef29d2c375e8..17c4902a1f507 100644 --- a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx @@ -17,18 +17,27 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; -import { CellOutputWebviewFactory, CellOutputWebview } from '../renderers/cell-output-webview'; import { NotebookRendererRegistry } from '../notebook-renderer-registry'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { NotebookModel } from '../view-model/notebook-model'; import { CellEditor } from './notebook-cell-editor'; -import { CellRenderer } from './notebook-cell-list-view'; +import { CellRenderer, observeCellHeight } from './notebook-cell-list-view'; import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory'; -import { NotebookCellActionContribution } from '../contributions/notebook-cell-actions-contribution'; +import { NotebookCellActionContribution, NotebookCellCommands } from '../contributions/notebook-cell-actions-contribution'; import { CellExecution, NotebookExecutionStateService } from '../service/notebook-execution-state-service'; import { codicon } from '@theia/core/lib/browser'; import { NotebookCellExecutionState } from '../../common'; -import { DisposableCollection } from '@theia/core'; +import { CancellationToken, CommandRegistry, DisposableCollection, nls } from '@theia/core'; +import { NotebookContextManager } from '../service/notebook-context-manager'; +import { NotebookViewportService } from './notebook-viewport-service'; +import { EditorPreferences } from '@theia/editor/lib/browser'; +import { NotebookOptionsService } from '../service/notebook-options'; +import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; +import { MarkdownString } from '@theia/monaco-editor-core/esm/vs/base/common/htmlContent'; +import { NotebookCellEditorService } from '../service/notebook-cell-editor-service'; +import { CellOutputWebview } from '../renderers/cell-output-webview'; +import { NotebookCellStatusBarItem, NotebookCellStatusBarItemList, NotebookCellStatusBarService } from '../service/notebook-cell-status-bar-service'; +import { LabelParser } from '@theia/core/lib/browser/label-parser'; @injectable() export class NotebookCodeCellRenderer implements CellRenderer { @@ -38,56 +47,211 @@ export class NotebookCodeCellRenderer implements CellRenderer { @inject(NotebookRendererRegistry) protected readonly notebookRendererRegistry: NotebookRendererRegistry; - @inject(CellOutputWebviewFactory) - protected readonly cellOutputWebviewFactory: CellOutputWebviewFactory; - @inject(NotebookCellToolbarFactory) protected readonly notebookCellToolbarFactory: NotebookCellToolbarFactory; @inject(NotebookExecutionStateService) protected readonly executionStateService: NotebookExecutionStateService; + @inject(NotebookContextManager) + protected readonly notebookContextManager: NotebookContextManager; + + @inject(NotebookViewportService) + protected readonly notebookViewportService: NotebookViewportService; + + @inject(EditorPreferences) + protected readonly editorPreferences: EditorPreferences; + + @inject(NotebookCellEditorService) + protected readonly notebookCellEditorService: NotebookCellEditorService; + + @inject(CommandRegistry) + protected readonly commandRegistry: CommandRegistry; + + @inject(NotebookOptionsService) + protected readonly notebookOptionsService: NotebookOptionsService; + + @inject(MarkdownRenderer) + protected readonly markdownRenderer: MarkdownRenderer; + + @inject(CellOutputWebview) + protected readonly outputWebview: CellOutputWebview; + + @inject(NotebookCellStatusBarService) + protected readonly notebookCellStatusBarService: NotebookCellStatusBarService; + + @inject(LabelParser) + protected readonly labelParser: LabelParser; + render(notebookModel: NotebookModel, cell: NotebookCellModel, handle: number): React.ReactNode { - return
    -
    -
    - {this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU, notebookModel, cell)} - {/* cell-execution-order needs an own component. Could be a little more complicated -

    {`[${cell.exec ?? ' '}]`}

    */} -
    -
    - - -
    -
    -
    - - this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.OUTPUT_SIDEBAR_MENU, notebookModel, cell, cell.outputs[0])} /> -
    + return
    observeCellHeight(ref, cell)}> +
    + + cell.requestFocusEditor()} /> +
    ; } + + renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { + return
    + + + this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.OUTPUT_SIDEBAR_MENU, cell, { + contextMenuArgs: () => [notebookModel, cell, cell.outputs[0]] + }) + } /> +
    ; + + } + + renderDragImage(cell: NotebookCellModel): HTMLElement { + const dragImage = document.createElement('div'); + dragImage.className = 'theia-notebook-drag-image'; + dragImage.style.width = this.notebookContextManager.context?.clientWidth + 'px'; + dragImage.style.height = '100px'; + dragImage.style.display = 'flex'; + + const fakeRunButton = document.createElement('span'); + fakeRunButton.className = `${codicon('play')} theia-notebook-cell-status-item`; + dragImage.appendChild(fakeRunButton); + + const fakeEditor = document.createElement('div'); + dragImage.appendChild(fakeEditor); + const lines = cell.source.split('\n').slice(0, 5).join('\n'); + const codeSequence = this.getMarkdownCodeSequence(lines); + const firstLine = new MarkdownString(`${codeSequence}${cell.language}\n${lines}\n${codeSequence}`, { supportHtml: true, isTrusted: false }); + fakeEditor.appendChild(this.markdownRenderer.render(firstLine).element); + fakeEditor.classList.add('theia-notebook-cell-editor-container'); + fakeEditor.style.padding = '10px'; + return dragImage; + } + + protected getMarkdownCodeSequence(input: string): string { + // We need a minimum of 3 backticks to start a code block. + let longest = 2; + let current = 0; + for (let i = 0; i < input.length; i++) { + const char = input.charAt(i); + if (char === '`') { + current++; + if (current > longest) { + longest = current; + } + } else { + current = 0; + } + } + return Array(longest + 1).fill('`').join(''); + } + +} + +export interface NotebookCodeCellSidebarProps { + cell: NotebookCellModel; + notebook: NotebookModel; + notebookCellToolbarFactory: NotebookCellToolbarFactory +} + +export class NotebookCodeCellSidebar extends React.Component { + + protected toDispose = new DisposableCollection(); + + constructor(props: NotebookCodeCellSidebarProps) { + super(props); + + this.toDispose.push(props.cell.onDidCellHeightChange(() => this.forceUpdate())); + } + + override componentWillUnmount(): void { + this.toDispose.dispose(); + } + + override render(): React.ReactNode { + return
    + {this.props.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU, this.props.cell, { + contextMenuArgs: () => [this.props.cell], commandArgs: () => [this.props.notebook, this.props.cell] + }) + } + +
    ; + } } export interface NotebookCodeCellStatusProps { + notebook: NotebookModel; cell: NotebookCellModel; - executionStateService: NotebookExecutionStateService + commandRegistry: CommandRegistry; + cellStatusBarService: NotebookCellStatusBarService; + executionStateService?: NotebookExecutionStateService; + labelParser: LabelParser; + onClick: () => void; +} + +export interface NotebookCodeCellStatusState { + currentExecution?: CellExecution; + executionTime: number; } -export class NotebookCodeCellStatus extends React.Component { +export class NotebookCodeCellStatus extends React.Component { protected toDispose = new DisposableCollection(); + protected statusBarItems: NotebookCellStatusBarItemList[] = []; + constructor(props: NotebookCodeCellStatusProps) { super(props); - this.state = {}; + this.state = { + executionTime: 0 + }; - this.toDispose.push(props.executionStateService.onDidChangeExecution(event => { - if (event.affectsCell(this.props.cell.uri)) { - this.setState({ currentExecution: event.changed }); - } + let currentInterval: NodeJS.Timeout | undefined; + if (props.executionStateService) { + this.toDispose.push(props.executionStateService.onDidChangeExecution(event => { + if (event.affectsCell(this.props.cell.uri)) { + this.setState({ currentExecution: event.changed, executionTime: 0 }); + clearInterval(currentInterval); + if (event.changed?.state === NotebookCellExecutionState.Executing) { + const startTime = Date.now(); + // The resolution of the time display is only a single digit after the decimal point. + // Therefore, we only need to update the display every 100ms. + currentInterval = setInterval(() => { + this.setState({ + executionTime: Date.now() - startTime + }); + }, 100); + } + } + })); + } + + this.toDispose.push(props.cell.onDidChangeLanguage(() => { + this.forceUpdate(); })); + + this.updateStatusBarItems(); + this.props.cellStatusBarService.onDidChangeItems(() => this.updateStatusBarItems()); + this.props.notebook.onContentChanged(() => this.updateStatusBarItems()); + } + + async updateStatusBarItems(): Promise { + this.statusBarItems = await this.props.cellStatusBarService.getStatusBarItemsForCell( + this.props.notebook.uri, + this.props.notebook.cells.indexOf(this.props.cell), + this.props.notebook.viewType, + CancellationToken.None); + this.forceUpdate(); } override componentWillUnmount(): void { @@ -95,17 +259,20 @@ export class NotebookCodeCellStatus extends React.Component + return
    this.props.onClick()}>
    - {this.renderExecutionState()} + {this.props.executionStateService && this.renderExecutionState()} + {this.statusBarItems?.length && this.renderStatusBarItems()}
    - {this.props.cell.language} + { + this.props.commandRegistry.executeCommand(NotebookCellCommands.CHANGE_CELL_LANGUAGE.id, this.props.notebook, this.props.cell); + }}>{this.props.cell.languageName}
    ; } - private renderExecutionState(): React.ReactNode { + protected renderExecutionState(): React.ReactNode { const state = this.state.currentExecution?.state; const { lastRunSuccess } = this.props.cell.internalMetadata; @@ -126,65 +293,114 @@ export class NotebookCodeCellStatus extends React.Component -
    {this.getExecutionTime()}
    +
    {this.renderTime(this.getExecutionTime())}
    } ; } - private getExecutionTime(): string { + protected getExecutionTime(): number { const { runStartTime, runEndTime } = this.props.cell.internalMetadata; - if (runStartTime && runEndTime) { - return `${((runEndTime - runStartTime) / 1000).toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 })}s`; + const { executionTime } = this.state; + if (runStartTime !== undefined && runEndTime !== undefined) { + return runEndTime - runStartTime; } - return '0.0s'; + return executionTime; + } + + protected renderTime(ms: number): string { + return `${(ms / 1000).toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 })}s`; + } + + protected renderStatusBarItems(): React.ReactNode { + return <> + { + this.statusBarItems.flatMap((itemList, listIndex) => + itemList.items.map((item, index) => this.renderStatusBarItem(item, `${listIndex}-${index}`) + ) + ) + } + ; + } + + protected renderStatusBarItem(item: NotebookCellStatusBarItem, key: string): React.ReactNode { + const content = this.props.labelParser.parse(item.text).map(part => { + if (typeof part === 'string') { + return part; + } else { + return ; + } + }); + return
    { + if (item.command) { + if (typeof item.command === 'string') { + this.props.commandRegistry.executeCommand(item.command); + } else { + this.props.commandRegistry.executeCommand(item.command.id, ...(item.command.arguments ?? [])); + } + } + }}> + {content} +
    ; } + } interface NotebookCellOutputProps { cell: NotebookCellModel; - outputWebviewFactory: CellOutputWebviewFactory; + notebook: NotebookModel; + outputWebview: CellOutputWebview; renderSidebar: () => React.ReactNode; } export class NotebookCodeCellOutputs extends React.Component { - protected outputsWebview: CellOutputWebview | undefined; + protected toDispose = new DisposableCollection(); - constructor(props: NotebookCellOutputProps) { - super(props); - } + protected outputHeight: number = 0; override async componentDidMount(): Promise { - const { cell, outputWebviewFactory } = this.props; - cell.onDidChangeOutputs(async () => { - if (!this.outputsWebview && cell.outputs.length > 0) { - this.outputsWebview = await outputWebviewFactory(cell); - } else if (this.outputsWebview && cell.outputs.length === 0) { - this.outputsWebview.dispose(); - this.outputsWebview = undefined; + const { cell } = this.props; + this.toDispose.push(cell.onDidChangeOutputs(() => this.forceUpdate())); + this.toDispose.push(this.props.cell.onDidChangeOutputVisibility(() => this.forceUpdate())); + this.toDispose.push(this.props.outputWebview.onDidRenderOutput(event => { + if (event.cellHandle === this.props.cell.handle) { + this.outputHeight = event.outputHeight; + this.forceUpdate(); } - this.forceUpdate(); - }); - if (cell.outputs.length > 0) { - this.outputsWebview = await outputWebviewFactory(cell); - this.forceUpdate(); - } + })); } - override componentDidUpdate(): void { - if (!this.outputsWebview?.isAttached()) { - this.outputsWebview?.attachWebview(); - } + override componentWillUnmount(): void { + this.toDispose.dispose(); } override render(): React.ReactNode { - return this.outputsWebview ? - <> + if (!this.props.cell.outputs?.length) { + return <>; + } + if (this.props.cell.outputVisible) { + return
    {this.props.renderSidebar()} - {this.outputsWebview.render()} - : - <>; - +
    ; + } + return
    {nls.localizeByDefault('Outputs are collapsed')}
    ; } } + +interface NotebookCellExecutionOrderProps { + cell: NotebookCellModel; +} + +function CodeCellExecutionOrder({ cell }: NotebookCellExecutionOrderProps): React.JSX.Element { + const [executionOrder, setExecutionOrder] = React.useState(cell.internalMetadata.executionOrder ?? ' '); + + React.useEffect(() => { + const listener = cell.onDidChangeInternalMetadata(e => { + setExecutionOrder(cell.internalMetadata.executionOrder ?? ' '); + }); + return () => listener.dispose(); + }, []); + + return {`[${executionOrder}]`}; +} diff --git a/packages/notebook/src/browser/view/notebook-find-widget.tsx b/packages/notebook/src/browser/view/notebook-find-widget.tsx new file mode 100644 index 0000000000000..be643c1a55509 --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-find-widget.tsx @@ -0,0 +1,335 @@ +// ***************************************************************************** +// 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 { nls } from '@theia/core'; +import * as React from '@theia/core/shared/react'; +import { codicon } from '@theia/core/lib/browser'; +import debounce = require('lodash/debounce'); + +export interface NotebookEditorFindMatch { + selected: boolean; + show(): void; + replace?(value: string): void; +} + +export interface NotebookEditorFindMatchOptions { + search: string; + matchCase: boolean; + wholeWord: boolean; + regex: boolean; + activeFilters: string[]; +} + +export interface NotebookEditorFindFilter { + id: string; + label: string; + active: boolean; +} + +export interface NotebookEditorFindOptions { + search?: string; + jumpToMatch?: boolean; + matchCase?: boolean; + wholeWord?: boolean; + regex?: boolean; + modifyIndex?: (matches: NotebookEditorFindMatch[], index: number) => number; +} + +export interface NotebookFindWidgetProps { + hidden?: boolean; + filters?: NotebookEditorFindFilter[]; + onClose(): void; + onSearch(options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[]; + onReplace(matches: NotebookEditorFindMatch[], value: string): void; +} + +export interface NotebookFindWidgetState { + search: string; + replace: string; + expanded: boolean; + matchCase: boolean; + wholeWord: boolean; + regex: boolean; + activeFilters: string[]; + currentMatch: number; + matches: NotebookEditorFindMatch[]; +} + +export class NotebookFindWidget extends React.Component { + + private searchRef = React.createRef(); + private debounceSearch = debounce(this.search.bind(this), 50); + + constructor(props: NotebookFindWidgetProps) { + super(props); + this.state = { + search: '', + replace: '', + currentMatch: 0, + matches: [], + expanded: false, + matchCase: false, + regex: false, + wholeWord: false, + activeFilters: props.filters?.filter(filter => filter.active).map(filter => filter.id) || [] + }; + } + + override render(): React.ReactNode { + const hasMatches = this.hasMatches(); + const canReplace = this.canReplace(); + const canReplaceAll = this.canReplaceAll(); + return ( +
    { + if (event.key === 'Escape') { + this.props.onClose(); + } + }} className={`theia-notebook-find-widget ${!this.state.expanded ? 'search-mode' : ''} ${this.props.hidden ? 'hidden' : ''}`}> +
    { + this.setState({ + expanded: !this.state.expanded + }); + }}> +
    +
    +
    +
    + { + this.setState({ + search: event.target.value + }); + this.debounceSearch({}); + }} + onKeyDown={event => { + if (event.key === 'Enter') { + if (event.shiftKey) { + this.gotoPreviousMatch(); + } else { + this.gotoNextMatch(); + } + event.preventDefault(); + } + }} + /> +
    { + this.search({ + matchCase: !this.state.matchCase + }); + }}>
    +
    { + this.search({ + wholeWord: !this.state.wholeWord + }); + }}>
    +
    { + this.search({ + regex: !this.state.regex + }); + }}>
    + {/*
    */} +
    + { + this.setState({ + replace: event.target.value + }); + }} + onKeyDown={event => { + if (event.key === 'Enter') { + this.replaceOne(); + event.preventDefault(); + } + }} + /> +
    +
    +
    +
    + {this.getMatchesCount()} +
    +
    { + this.gotoPreviousMatch(); + }} + >
    +
    { + this.gotoNextMatch(); + }} + >
    +
    { + this.props.onClose(); + }} + >
    +
    +
    +
    { + this.replaceOne(); + }} + >
    +
    { + this.replaceAll(); + }} + >
    +
    +
    +
    + ); + } + + private hasMatches(): boolean { + return this.state.matches.length > 0; + } + + private canReplace(): boolean { + return Boolean(this.state.matches[this.state.currentMatch]?.replace); + } + + private canReplaceAll(): boolean { + return this.state.matches.some(match => Boolean(match.replace)); + } + + private getMatchesCount(): string { + if (this.hasMatches()) { + return nls.localizeByDefault('{0} of {1}', this.state.currentMatch + 1, this.state.matches.length); + } else { + return nls.localizeByDefault('No results'); + } + } + + private gotoNextMatch(): void { + this.search({ + modifyIndex: (matches, index) => (index + 1) % matches.length, + jumpToMatch: true + }); + } + + private gotoPreviousMatch(): void { + this.search({ + modifyIndex: (matches, index) => (index === 0 ? matches.length : index) - 1, + jumpToMatch: true + }); + } + + private replaceOne(): void { + const existingMatches = this.state.matches; + const match = existingMatches[this.state.currentMatch]; + if (match) { + match.replace?.(this.state.replace); + this.search({ + jumpToMatch: true, + modifyIndex: (matches, index) => { + if (matches.length < existingMatches.length) { + return index % matches.length; + } else { + const diff = matches.length - existingMatches.length; + return (index + diff + 1) % matches.length; + } + } + }); + } + } + + private replaceAll(): void { + this.props.onReplace(this.state.matches, this.state.replace); + this.search({}); + } + + override componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { + if (!this.props.hidden && prevProps.hidden) { + // Focus the search input when the widget switches from hidden to visible. + this.searchRef.current?.focus(); + } + } + + focusSearch(content?: string): void { + this.searchRef.current?.focus(); + if (content) { + this.search({ + search: content, + jumpToMatch: false + }); + } + } + + search(options: NotebookEditorFindOptions): void { + const matchCase = options.matchCase ?? this.state.matchCase; + const wholeWord = options.wholeWord ?? this.state.wholeWord; + const regex = options.regex ?? this.state.regex; + const search = options.search ?? this.state.search; + const matches = this.props.onSearch({ + search, + matchCase, + wholeWord, + regex, + activeFilters: this.state.activeFilters + }); + let currentMatch = Math.max(0, Math.min(this.state.currentMatch, matches.length - 1)); + if (options.modifyIndex && matches.length > 0) { + currentMatch = options.modifyIndex(matches, currentMatch); + } + const selectedMatch = matches[currentMatch]; + if (selectedMatch) { + selectedMatch.selected = true; + if (options.jumpToMatch) { + selectedMatch.show(); + } + } + this.setState({ + search, + matches, + currentMatch, + matchCase, + wholeWord, + regex + }); + } + +} diff --git a/packages/notebook/src/browser/view/notebook-main-toolbar.tsx b/packages/notebook/src/browser/view/notebook-main-toolbar.tsx index bc12319b10ca5..59b04784d8b4e 100644 --- a/packages/notebook/src/browser/view/notebook-main-toolbar.tsx +++ b/packages/notebook/src/browser/view/notebook-main-toolbar.tsx @@ -13,14 +13,16 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { CommandRegistry, CompoundMenuNodeRole, DisposableCollection, MenuModelRegistry, MenuNode, nls } from '@theia/core'; +import { ArrayUtils, CommandRegistry, CompoundMenuNodeRole, DisposableCollection, MenuModelRegistry, MenuNode, nls } from '@theia/core'; import * as React from '@theia/core/shared/react'; -import { codicon } from '@theia/core/lib/browser'; +import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser'; import { NotebookCommands, NotebookMenus } from '../contributions/notebook-actions-contribution'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookKernelService } from '../service/notebook-kernel-service'; import { inject, injectable } from '@theia/core/shared/inversify'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { NotebookCommand } from '../../common'; +import { NotebookContextManager } from '../service/notebook-context-manager'; export interface NotebookMainToolbarProps { notebookModel: NotebookModel @@ -28,6 +30,9 @@ export interface NotebookMainToolbarProps { notebookKernelService: NotebookKernelService; commandRegistry: CommandRegistry; contextKeyService: ContextKeyService; + editorNode: HTMLElement; + notebookContextManager: NotebookContextManager; + contextMenuRenderer: ContextMenuRenderer; } @injectable() @@ -36,25 +41,49 @@ export class NotebookMainToolbarRenderer { @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry; @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; + @inject(NotebookContextManager) protected readonly notebookContextManager: NotebookContextManager; + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; - render(notebookModel: NotebookModel): React.ReactNode { + render(notebookModel: NotebookModel, editorNode: HTMLElement): React.ReactNode { return ; + editorNode={editorNode} + notebookContextManager={this.notebookContextManager} + contextMenuRenderer={this.contextMenuRenderer} />; } } -export class NotebookMainToolbar extends React.Component { +interface NotebookMainToolbarState { + selectedKernelLabel?: string; + numberOfHiddenItems: number; +} + +export class NotebookMainToolbar extends React.Component { + + // The minimum area between items and kernel select before hiding items in a context menu + static readonly MIN_FREE_AREA = 10; protected toDispose = new DisposableCollection(); + protected nativeSubmenus = [ + NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP[NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP.length - 1], + NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP[NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP.length - 1]]; + + protected gapElement: HTMLDivElement | undefined; + protected lastGapElementWidth: number = 0; + + protected resizeObserver: ResizeObserver = new ResizeObserver(() => this.calculateItemsToHide()); + constructor(props: NotebookMainToolbarProps) { super(props); - this.state = { selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label }; + this.state = { + selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label, + numberOfHiddenItems: 0, + }; this.toDispose.push(props.notebookKernelService.onDidChangeSelectedKernel(event => { if (props.notebookModel.uri.isEqual(event.notebook)) { this.setState({ selectedKernelLabel: props.notebookKernelService.getKernel(event.newKernel ?? '')?.label }); @@ -66,50 +95,141 @@ export class NotebookMainToolbar extends React.Component(); + this.getAllContextKeys(this.getMenuItems(), contextKeys); + props.notebookContextManager.onDidChangeContext(e => { + if (e.affects(contextKeys)) { + this.forceUpdate(); + } + }); + props.contextKeyService.onDidChange(e => { + if (e.affects(contextKeys)) { + this.forceUpdate(); + } + }); + } override componentWillUnmount(): void { this.toDispose.dispose(); } + override componentDidUpdate(): void { + this.calculateItemsToHide(); + } + + override componentDidMount(): void { + this.calculateItemsToHide(); + } + + protected calculateItemsToHide(): void { + const numberOfMenuItems = this.getMenuItems().length; + if (this.gapElement && this.gapElement.getBoundingClientRect().width < NotebookMainToolbar.MIN_FREE_AREA && this.state.numberOfHiddenItems < numberOfMenuItems) { + this.setState({ ...this.state, numberOfHiddenItems: this.state.numberOfHiddenItems + 1 }); + this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width; + } else if (this.gapElement && this.gapElement.getBoundingClientRect().width > this.lastGapElementWidth && this.state.numberOfHiddenItems > 0) { + this.setState({ ...this.state, numberOfHiddenItems: 0 }); + this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width; + } + } + + protected renderContextMenu(event: MouseEvent, menuItems: readonly MenuNode[]): void { + const hiddenItems = menuItems.slice(menuItems.length - this.calculateNumberOfHiddenItems(menuItems)); + const contextMenu = this.props.menuRegistry.getMenu([NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU]); + + contextMenu.children.map(item => item.id).forEach(id => contextMenu.removeNode(id)); + hiddenItems.forEach(item => contextMenu.addNode(item)); + + this.props.contextMenuRenderer.render({ + anchor: event, + menuPath: [NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU], + context: this.props.editorNode, + args: [this.props.notebookModel.uri] + }); + } + override render(): React.ReactNode { - return
    - {this.getMenuItems().map(item => this.renderMenuItem(item))} -
    -
    + {menuItems.slice(0, menuItems.length - this.calculateNumberOfHiddenItems(menuItems)).map(item => this.renderMenuItem(item))} + { + this.state.numberOfHiddenItems > 0 && + this.renderContextMenu(e.nativeEvent, menuItems)} /> + } +
    this.gapElementChanged(element)} style={{ flexGrow: 1 }}>
    +
    this.props.commandRegistry.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, this.props.notebookModel)}> - + {this.state.selectedKernelLabel ?? nls.localizeByDefault('Select Kernel')}
    -
    ; +
    ; + } + + protected gapElementChanged(element: HTMLDivElement | null): void { + if (this.gapElement) { + this.resizeObserver.unobserve(this.gapElement); + } + this.gapElement = element ?? undefined; + if (this.gapElement) { + this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width; + this.resizeObserver.observe(this.gapElement); + } } - protected renderMenuItem(item: MenuNode): React.ReactNode { + protected renderMenuItem(item: MenuNode, submenu?: string): React.ReactNode { if (item.role === CompoundMenuNodeRole.Group) { - const itemNodes = item.children?.map(child => this.renderMenuItem(child)).filter(child => !!child); + const itemNodes = ArrayUtils.coalesce(item.children?.map(child => this.renderMenuItem(child, item.id)) ?? []); return {itemNodes} {itemNodes && itemNodes.length > 0 && } ; - } else if (!item.when || this.props.contextKeyService.match(item.when)) { - return
    { - if (item.command) { - this.props.commandRegistry.executeCommand(item.command, this.props.notebookModel); + if (item.command && (!item.when || this.props.contextKeyService.match(item.when, this.props.editorNode))) { + this.props.commandRegistry.executeCommand(item.command, this.props.notebookModel.uri); } }}> - {item.label} + {label}
    ; } return undefined; } - private getMenuItems(): readonly MenuNode[] { + protected getMenuItems(): readonly MenuNode[] { const menuPath = NotebookMenus.NOTEBOOK_MAIN_TOOLBAR; const pluginCommands = this.props.menuRegistry.getMenuNode(menuPath).children; - return this.props.menuRegistry.getMenu([menuPath]).children.concat(pluginCommands); + const theiaCommands = this.props.menuRegistry.getMenu([menuPath]).children; + return theiaCommands.concat(pluginCommands); + } + + protected getAdditionalClasses(item: MenuNode): string { + return !item.when || this.props.contextKeyService.match(item.when, this.props.editorNode) ? '' : ' theia-mod-disabled'; + } + + protected getAllContextKeys(menus: readonly MenuNode[], keySet: Set): void { + menus.filter(item => item.when) + .forEach(item => this.props.contextKeyService.parseKeys(item.when!)?.forEach(key => keySet.add(key))); + + menus.filter(item => item.children && item.children.length > 0) + .forEach(item => this.getAllContextKeys(item.children!, keySet)); + } + + protected calculateNumberOfHiddenItems(allMenuItems: readonly MenuNode[]): number { + return this.state.numberOfHiddenItems >= allMenuItems.length ? + allMenuItems.length : + this.state.numberOfHiddenItems % allMenuItems.length; } } diff --git a/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx index af84d9fe361fc..c0397fe0649e2 100644 --- a/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx @@ -18,12 +18,20 @@ import * as React from '@theia/core/shared/react'; import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string'; import { NotebookModel } from '../view-model/notebook-model'; -import { CellRenderer } from './notebook-cell-list-view'; +import { CellRenderer, observeCellHeight } from './notebook-cell-list-view'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { CellEditor } from './notebook-cell-editor'; import { inject, injectable } from '@theia/core/shared/inversify'; import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; -import { nls } from '@theia/core'; +import { CommandRegistry, nls } from '@theia/core'; +import { NotebookContextManager } from '../service/notebook-context-manager'; +import { NotebookOptionsService } from '../service/notebook-options'; +import { NotebookCodeCellStatus } from './notebook-code-cell-view'; +import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from './notebook-find-widget'; +import * as mark from 'advanced-mark.js'; +import { NotebookCellEditorService } from '../service/notebook-cell-editor-service'; +import { NotebookCellStatusBarService } from '../service/notebook-cell-status-bar-service'; +import { LabelParser } from '@theia/core/lib/browser/label-parser'; @injectable() export class NotebookMarkdownCellRenderer implements CellRenderer { @@ -33,41 +41,199 @@ export class NotebookMarkdownCellRenderer implements CellRenderer { @inject(MonacoEditorServices) protected readonly monacoServices: MonacoEditorServices; + @inject(NotebookContextManager) + protected readonly notebookContextManager: NotebookContextManager; + + @inject(CommandRegistry) + protected readonly commandRegistry: CommandRegistry; + + @inject(NotebookOptionsService) + protected readonly notebookOptionsService: NotebookOptionsService; + + @inject(NotebookCellEditorService) + protected readonly notebookCellEditorService: NotebookCellEditorService; + + @inject(NotebookCellStatusBarService) + protected readonly notebookCellStatusBarService: NotebookCellStatusBarService; + + @inject(LabelParser) + protected readonly labelParser: LabelParser; + render(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { - return ; + return ; } + renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { + return
    ; + } + + renderDragImage(cell: NotebookCellModel): HTMLElement { + const dragImage = document.createElement('div'); + dragImage.style.width = this.notebookContextManager.context?.clientWidth + 'px'; + const markdownString = new MarkdownStringImpl(cell.source, { supportHtml: true, isTrusted: true }); + const markdownElement = this.markdownRenderer.render(markdownString).element; + dragImage.appendChild(markdownElement); + return dragImage; + } } interface MarkdownCellProps { - markdownRenderer: MarkdownRenderer, - monacoServices: MonacoEditorServices + markdownRenderer: MarkdownRenderer; + monacoServices: MonacoEditorServices; - cell: NotebookCellModel, - notebookModel: NotebookModel + commandRegistry: CommandRegistry; + cell: NotebookCellModel; + notebookModel: NotebookModel; + notebookContextManager: NotebookContextManager; + notebookOptionsService: NotebookOptionsService; + notebookCellEditorService: NotebookCellEditorService; + notebookCellStatusBarService: NotebookCellStatusBarService; + labelParser: LabelParser; } -function MarkdownCell({ markdownRenderer, monacoServices, cell, notebookModel }: MarkdownCellProps): React.JSX.Element { - const [editMode, setEditMode] = React.useState(false); +function MarkdownCell({ + markdownRenderer, monacoServices, cell, notebookModel, notebookContextManager, + notebookOptionsService, commandRegistry, notebookCellEditorService, notebookCellStatusBarService, + labelParser +}: MarkdownCellProps): React.JSX.Element { + const [editMode, setEditMode] = React.useState(cell.editing); + let empty = false; React.useEffect(() => { const listener = cell.onDidRequestCellEditChange(cellEdit => setEditMode(cellEdit)); return () => listener.dispose(); }, [editMode]); - let markdownContent = React.useMemo(() => markdownRenderer.render(new MarkdownStringImpl(cell.source)).element.innerHTML, [cell, editMode]); + React.useEffect(() => { + if (!editMode) { + const instance = new mark(markdownContent); + cell.onMarkdownFind = options => { + instance.unmark(); + if (empty) { + return []; + } + return searchInMarkdown(instance, options); + }; + return () => { + cell.onMarkdownFind = undefined; + instance.unmark(); + }; + } + }, [editMode, cell.source]); + + let markdownContent: HTMLElement[] = React.useMemo(() => { + const markdownString = new MarkdownStringImpl(cell.source, { supportHtml: true, isTrusted: true }); + const rendered = markdownRenderer.render(markdownString).element; + const children: HTMLElement[] = []; + rendered.childNodes.forEach(child => { + if (child instanceof HTMLElement) { + children.push(child); + } + }); + return children; + }, [cell.source]); + if (markdownContent.length === 0) { - markdownContent = `${nls.localizeByDefault('Empty markdown cell, double-click or press enter to edit.')}`; + const italic = document.createElement('i'); + italic.className = 'theia-notebook-empty-markdown'; + italic.innerText = nls.localizeByDefault('Empty markdown cell, double-click or press enter to edit.'); + italic.style.pointerEvents = 'none'; + markdownContent = [italic]; + empty = true; } return editMode ? - : -
    observeCellHeight(ref, cell)}> + + cell.requestFocusEditor()} /> +
    ) : + (
    cell.requestEdit()} - // This sets the non React HTML node from the markdown renderers output as a child node to this react component - // This is currently sadly the best way we have to combine React (Virtual Nodes) and normal dom nodes - // the HTML is already sanitized by the markdown renderer, so we don't need to sanitize it again - dangerouslySetInnerHTML={{ __html: markdownContent }} // eslint-disable-line react/no-danger - />; + ref={node => { + node?.replaceChildren(...markdownContent); + observeCellHeight(node, cell); + }} + />); +} + +function searchInMarkdown(instance: mark, options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[] { + const matches: NotebookEditorFindMatch[] = []; + const markOptions: mark.MarkOptions & mark.RegExpOptions = { + className: 'theia-find-match', + diacritics: false, + caseSensitive: options.matchCase, + acrossElements: true, + separateWordSearch: false, + each: node => { + matches.push(new MarkdownEditorFindMatch(node)); + } + }; + if (options.regex || options.wholeWord) { + let search = options.search; + if (options.wholeWord) { + if (!options.regex) { + search = escapeRegExp(search); + } + search = '\\b' + search + '\\b'; + } + instance.markRegExp(new RegExp(search, options.matchCase ? '' : 'i'), markOptions); + } else { + instance.mark(options.search, markOptions); + } + return matches; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +class MarkdownEditorFindMatch implements NotebookEditorFindMatch { + + constructor(readonly node: Node) { } + + private _selected = false; + + get selected(): boolean { + return this._selected; + } + + set selected(selected: boolean) { + this._selected = selected; + const className = 'theia-find-match-selected'; + if (this.node instanceof HTMLElement) { + if (selected) { + this.node.classList.add(className); + } else { + this.node.classList.remove(className); + } + } + } + + show(): void { + if (this.node instanceof HTMLElement) { + this.node.scrollIntoView({ + behavior: 'instant', + block: 'center' + }); + } + } } diff --git a/packages/notebook/src/browser/view/notebook-viewport-service.ts b/packages/notebook/src/browser/view/notebook-viewport-service.ts new file mode 100644 index 0000000000000..93dc2a2ccf859 --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-viewport-service.ts @@ -0,0 +1,61 @@ +// ***************************************************************************** +// 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 { Disposable } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; + +/** + * this service is for managing the viewport and scroll state of a notebook editor. + * its used both for restoring scroll state after reopening an editor and for cell to check if they are in the viewport. + */ +@injectable() +export class NotebookViewportService implements Disposable { + + protected onDidChangeViewportEmitter = new Emitter(); + readonly onDidChangeViewport = this.onDidChangeViewportEmitter.event; + + protected _viewportElement: HTMLDivElement | undefined; + + protected resizeObserver: ResizeObserver; + + set viewportElement(element: HTMLDivElement | undefined) { + this._viewportElement = element; + if (element) { + this.onDidChangeViewportEmitter.fire(); + this.resizeObserver?.disconnect(); + this.resizeObserver = new ResizeObserver(() => this.onDidChangeViewportEmitter.fire()); + this.resizeObserver.observe(element); + } + } + + isElementInViewport(element: HTMLElement): boolean { + if (this._viewportElement) { + const rect = element.getBoundingClientRect(); + const viewRect = this._viewportElement.getBoundingClientRect(); + return rect.top < viewRect.top ? rect.bottom > viewRect.top : rect.top < viewRect.bottom; + } + return false; + } + + onScroll(e: HTMLDivElement): void { + this.onDidChangeViewportEmitter.fire(); + } + + dispose(): void { + this.resizeObserver.disconnect(); + } +} diff --git a/packages/notebook/src/common/notebook-common.ts b/packages/notebook/src/common/notebook-common.ts index 7fb5a4dbc26cc..58e8fdc8ada9b 100644 --- a/packages/notebook/src/common/notebook-common.ts +++ b/packages/notebook/src/common/notebook-common.ts @@ -14,11 +14,17 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { CancellationToken, Command, Event, URI } from '@theia/core'; +import { Command, URI, isObject } from '@theia/core'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { UriComponents } from '@theia/core/lib/common/uri'; -import { CellRange } from './notebook-range'; + +export interface NotebookCommand extends Command { + title?: string; + tooltip?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + arguments?: any[]; +} export enum CellKind { Markup = 1, @@ -88,23 +94,6 @@ export interface NotebookCellCollapseState { outputCollapsed?: boolean; } -export interface NotebookCell { - readonly uri: URI; - handle: number; - language: string; - cellKind: CellKind; - outputs: CellOutput[]; - metadata: NotebookCellMetadata; - internalMetadata: NotebookCellInternalMetadata; - text: string; - onDidChangeOutputs?: Event; - onDidChangeOutputItems?: Event; - onDidChangeLanguage: Event; - onDidChangeMetadata: Event; - onDidChangeInternalMetadata: Event; - -} - export interface CellData { source: string; language: string; @@ -115,13 +104,6 @@ export interface CellData { collapseState?: NotebookCellCollapseState; } -export interface CellReplaceEdit { - editType: CellEditType.Replace; - index: number; - count: number; - cells: CellData[]; -} - export interface NotebookDocumentMetadataEdit { editType: CellEditType.DocumentMetadata; metadata: NotebookDocumentMetadata; @@ -140,32 +122,11 @@ export interface NotebookContributionData { exclusive: boolean; } -export interface NotebookCellStatusBarItemList { - items: NotebookCellStatusBarItem[]; - dispose?(): void; -} - -export interface NotebookCellStatusBarItemProvider { - viewType: string; - onDidChangeStatusBarItems?: Event; - provideCellStatusBarItems(uri: UriComponents, index: number, token: CancellationToken): Promise; -} - -export interface NotebookCellOutputsSplice { - start: number /* start */; - deleteCount: number /* delete count */; - newOutputs: CellOutput[]; -}; - -export interface CellInternalMetadataChangedEvent { - readonly lastRunSuccessChanged?: boolean; -} - -export type NotebookCellTextModelSplice = [ +export interface NotebookCellTextModelSplice { start: number, deleteCount: number, newItems: T[] -]; +}; export enum NotebookCellsChangeType { ModelChange = 1, @@ -182,69 +143,12 @@ export enum NotebookCellsChangeType { Unknown = 100 } -export enum SelectionStateType { - Handle = 0, - Index = 1 -} -export interface SelectionHandleState { - kind: SelectionStateType.Handle; - primary: number | null; - selections: number[]; -} - -export interface SelectionIndexState { - kind: SelectionStateType.Index; - focus: CellRange; - selections: CellRange[]; -} - -export type SelectionState = SelectionHandleState | SelectionIndexState; - -export interface NotebookTextModelChangedEvent { - readonly rawEvents: NotebookRawContentEvent[]; - // readonly versionId: number; - readonly synchronous?: boolean; - readonly endSelectionState?: SelectionState; -}; - -export interface NotebookCellsInitializeEvent { - readonly kind: NotebookCellsChangeType.Initialize; - readonly changes: NotebookCellTextModelSplice[]; -} - export interface NotebookCellsChangeLanguageEvent { readonly kind: NotebookCellsChangeType.ChangeCellLanguage; readonly index: number; readonly language: string; } -export interface NotebookCellsModelChangedEvent { - readonly kind: NotebookCellsChangeType.ModelChange; - readonly changes: NotebookCellTextModelSplice[]; -} - -export interface NotebookCellsModelMoveEvent { - readonly kind: NotebookCellsChangeType.Move; - readonly index: number; - readonly length: number; - readonly newIdx: number; - readonly cells: T[]; -} - -export interface NotebookOutputChangedEvent { - readonly kind: NotebookCellsChangeType.Output; - readonly index: number; - readonly outputs: CellOutput[]; - readonly append: boolean; -} - -export interface NotebookOutputItemChangedEvent { - readonly kind: NotebookCellsChangeType.OutputItem; - readonly index: number; - readonly outputId: string; - readonly outputItems: CellOutputItem[]; - readonly append: boolean; -} export interface NotebookCellsChangeMetadataEvent { readonly kind: NotebookCellsChangeType.ChangeCellMetadata; readonly index: number; @@ -257,35 +161,36 @@ export interface NotebookCellsChangeInternalMetadataEvent { readonly internalMetadata: NotebookCellInternalMetadata; } -export interface NotebookDocumentChangeMetadataEvent { - readonly kind: NotebookCellsChangeType.ChangeDocumentMetadata; - readonly metadata: NotebookDocumentMetadata; -} - -export interface NotebookDocumentUnknownChangeEvent { - readonly kind: NotebookCellsChangeType.Unknown; -} - export interface NotebookCellContentChangeEvent { readonly kind: NotebookCellsChangeType.ChangeCellContent; readonly index: number; } -export type NotebookRawContentEvent = (NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | - NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookOutputItemChangedEvent | - NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent | - NotebookCellsChangeInternalMetadataEvent | NotebookDocumentUnknownChangeEvent); // & { transient: boolean }; +export interface NotebookModelResource { + notebookModelUri: URI; +} -export interface NotebookModelChangedEvent { - readonly rawEvents: NotebookRawContentEvent[]; - readonly versionId: number; - // readonly synchronous: boolean | undefined; - // readonly endSelectionState: ISelectionState | undefined; -}; +export namespace NotebookModelResource { + export function is(item: unknown): item is NotebookModelResource { + return isObject(item) && item.notebookModelUri instanceof URI; + } + export function create(uri: URI): NotebookModelResource { + return { notebookModelUri: uri }; + } +} -export interface NotebookModelWillAddRemoveEvent { - readonly rawEvent: NotebookCellsModelChangedEvent; -}; +export interface NotebookCellModelResource { + notebookCellModelUri: URI; +} + +export namespace NotebookCellModelResource { + export function is(item: unknown): item is NotebookCellModelResource { + return isObject(item) && item.notebookCellModelUri instanceof URI; + } + export function create(uri: URI): NotebookCellModelResource { + return { notebookCellModelUri: uri }; + } +} export enum NotebookCellExecutionState { Unconfirmed = 1, @@ -321,51 +226,12 @@ export interface CellExecutionStateUpdateDto { isPaused?: boolean; } -export interface CellOutputEdit { - editType: CellEditType.Output; - index: number; - outputs: CellOutput[]; - append?: boolean; -} - -export interface CellOutputEditByHandle { - editType: CellEditType.Output; - handle: number; - outputs: CellOutput[]; - append?: boolean; -} - -export interface CellOutputItemEdit { - editType: CellEditType.OutputItems; - items: CellOutputItem[]; - outputId: string; - append?: boolean; -} - export interface CellMetadataEdit { editType: CellEditType.Metadata; index: number; metadata: NotebookCellMetadata; } -export interface CellLanguageEdit { - editType: CellEditType.CellLanguage; - index: number; - language: string; -} - -export interface DocumentMetadataEdit { - editType: CellEditType.DocumentMetadata; - metadata: NotebookDocumentMetadata; -} - -export interface CellMoveEdit { - editType: CellEditType.Move; - index: number; - length: number; - newIdx: number; -} - export const enum CellEditType { Replace = 1, Output = 2, @@ -378,19 +244,6 @@ export const enum CellEditType { PartialInternalMetadata = 9, } -export type ImmediateCellEditOperation = CellOutputEditByHandle | CellOutputItemEdit | CellPartialInternalMetadataEditByHandle; // add more later on -export type CellEditOperation = ImmediateCellEditOperation | CellReplaceEdit | CellOutputEdit | - CellMetadataEdit | CellLanguageEdit | DocumentMetadataEdit | CellMoveEdit; // add more later on - -export type NullablePartialNotebookCellInternalMetadata = { - [Key in keyof Partial]: NotebookCellInternalMetadata[Key] | null -}; -export interface CellPartialInternalMetadataEditByHandle { - editType: CellEditType.PartialInternalMetadata; - handle: number; - internalMetadata: NullablePartialNotebookCellInternalMetadata; -} - export interface NotebookKernelSourceAction { readonly label: string; readonly description?: string; @@ -408,7 +261,8 @@ export function isTextStreamMime(mimeType: string): boolean { export namespace CellUri { - export const scheme = 'vscode-notebook-cell'; + export const cellUriScheme = 'vscode-notebook-cell'; + export const outputUriScheme = 'vscode-notebook-cell-output'; const _lengths = ['W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f']; const _padRegexp = new RegExp(`^[${_lengths.join('')}]+`); @@ -419,12 +273,12 @@ export namespace CellUri { const s = handle.toString(_radix); const p = s.length < _lengths.length ? _lengths[s.length - 1] : 'z'; - const fragment = `${p}${s}s${Buffer.from(BinaryBuffer.fromString(notebook.scheme).buffer).toString('base64')} `; - return notebook.withScheme(scheme).withFragment(fragment); + const fragment = `${p}${s}s${Buffer.from(BinaryBuffer.fromString(notebook.scheme).buffer).toString('base64')}`; + return notebook.withScheme(cellUriScheme).withFragment(fragment); } export function parse(cell: URI): { notebook: URI; handle: number } | undefined { - if (cell.scheme !== scheme) { + if (cell.scheme !== cellUriScheme) { return undefined; } @@ -445,6 +299,30 @@ export namespace CellUri { }; } + export function generateCellOutputUri(notebook: URI, outputId?: string): URI { + return notebook + .withScheme(outputUriScheme) + .withQuery(`op${outputId ?? ''},${notebook.scheme !== 'file' ? notebook.scheme : ''}`); + }; + + export function parseCellOutputUri(uri: URI): { notebook: URI; outputId?: string } | undefined { + if (uri.scheme !== outputUriScheme) { + return; + } + + const match = /^op([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})?\,(.*)$/i.exec(uri.query); + if (!match) { + return undefined; + } + + const outputId = match[1] || undefined; + const scheme = match[2]; + return { + outputId, + notebook: uri.withScheme(scheme || 'file').withoutQuery() + }; + } + export function generateCellPropertyUri(notebook: URI, handle: number, cellScheme: string): URI { return CellUri.generate(notebook, handle).withScheme(cellScheme); } @@ -454,6 +332,6 @@ export namespace CellUri { return undefined; } - return CellUri.parse(uri.withScheme(scheme)); + return CellUri.parse(uri.withScheme(cellUriScheme)); } } diff --git a/packages/notebook/tsconfig.json b/packages/notebook/tsconfig.json index 8f53c0fe2dd53..6c992299de5cc 100644 --- a/packages/notebook/tsconfig.json +++ b/packages/notebook/tsconfig.json @@ -20,6 +20,9 @@ }, { "path": "../monaco" + }, + { + "path": "../outline-view" } ] } diff --git a/packages/outline-view/package.json b/packages/outline-view/package.json index e48c7a3ad9a80..d7ce97cab677c 100644 --- a/packages/outline-view/package.json +++ b/packages/outline-view/package.json @@ -1,9 +1,10 @@ { "name": "@theia/outline-view", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Outline View Extension", "dependencies": { - "@theia/core": "1.44.0" + "@theia/core": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -38,7 +39,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx b/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx index 285d76cc928c3..530e194a49e08 100644 --- a/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx +++ b/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx @@ -30,10 +30,14 @@ export interface BreadcrumbPopupOutlineViewFactory { export class BreadcrumbPopupOutlineView extends OutlineViewWidget { @inject(OpenerService) protected readonly openerService: OpenerService; + @inject(OutlineViewService) + protected readonly outlineViewService: OutlineViewService; + protected override tapNode(node?: TreeNode): void { if (UriSelection.is(node) && OutlineSymbolInformationNode.hasRange(node)) { open(this.openerService, node.uri, { selection: node.range }); } else { + this.outlineViewService.didTapNode(node as OutlineSymbolInformationNode); super.tapNode(node); } } diff --git a/packages/outline-view/src/browser/outline-view-service.ts b/packages/outline-view/src/browser/outline-view-service.ts index 278813371f661..dd2f58465a0f7 100644 --- a/packages/outline-view/src/browser/outline-view-service.ts +++ b/packages/outline-view/src/browser/outline-view-service.ts @@ -30,6 +30,7 @@ export class OutlineViewService implements WidgetFactory { protected readonly onDidChangeOpenStateEmitter = new Emitter(); protected readonly onDidSelectEmitter = new Emitter(); protected readonly onDidOpenEmitter = new Emitter(); + protected readonly onDidTapNodeEmitter = new Emitter(); constructor(@inject(OutlineViewWidgetFactory) protected factory: OutlineViewWidgetFactory) { } @@ -49,10 +50,18 @@ export class OutlineViewService implements WidgetFactory { return this.onDidChangeOpenStateEmitter.event; } + get onDidTapNode(): Event { + return this.onDidTapNodeEmitter.event; + } + get open(): boolean { return this.widget !== undefined && this.widget.isVisible; } + didTapNode(node: OutlineSymbolInformationNode): void { + this.onDidTapNodeEmitter.fire(node); + } + /** * Publish the collection of outline view symbols. * - Publishing includes setting the `OutlineViewWidget` tree with symbol information. diff --git a/packages/output/package.json b/packages/output/package.json index cf70145d85283..ef2c8059bcf49 100644 --- a/packages/output/package.json +++ b/packages/output/package.json @@ -1,14 +1,15 @@ { "name": "@theia/output", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Output Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/monaco": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/monaco": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", "@types/p-queue": "^2.3.1", - "p-queue": "^2.4.2" + "p-queue": "^2.4.2", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -43,7 +44,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/output/src/browser/output-editor-factory.ts b/packages/output/src/browser/output-editor-factory.ts index 4c1706ec68e81..c78803da024a7 100644 --- a/packages/output/src/browser/output-editor-factory.ts +++ b/packages/output/src/browser/output-editor-factory.ts @@ -35,10 +35,10 @@ export class OutputEditorFactory implements MonacoEditorFactory { readonly scheme: string = OutputUri.SCHEME; - create(model: MonacoEditorModel, defaultsOptions: MonacoEditor.IOptions, defaultOverrides: EditorServiceOverrides): MonacoEditor { + create(model: MonacoEditorModel, defaultsOptions: MonacoEditor.IOptions): MonacoEditor { const uri = new URI(model.uri); const options = this.createOptions(model, defaultsOptions); - const overrides = this.createOverrides(model, defaultOverrides); + const overrides = this.createOverrides(model); return new MonacoEditor(uri, model, document.createElement('div'), this.services, options, overrides); } @@ -62,13 +62,7 @@ export class OutputEditorFactory implements MonacoEditorFactory { }; } - protected *createOverrides(model: MonacoEditorModel, defaultOverrides: EditorServiceOverrides): EditorServiceOverrides { + protected *createOverrides(model: MonacoEditorModel): EditorServiceOverrides { yield [IContextMenuService, this.contextMenuService]; - for (const [identifier, provider] of defaultOverrides) { - if (identifier !== IContextMenuService) { - yield [identifier, provider]; - } - } } - } diff --git a/packages/plugin-dev/package.json b/packages/plugin-dev/package.json index 8644d6274e2c9..eff7071bcf6d1 100644 --- a/packages/plugin-dev/package.json +++ b/packages/plugin-dev/package.json @@ -1,17 +1,18 @@ { "name": "@theia/plugin-dev", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Plugin Development Extension", "main": "lib/common/index.js", "typings": "lib/common/index.d.ts", "dependencies": { - "@theia/core": "1.44.0", - "@theia/debug": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/output": "1.44.0", - "@theia/plugin-ext": "1.44.0", - "@theia/workspace": "1.44.0", - "ps-tree": "^1.2.0" + "@theia/core": "1.54.0", + "@theia/debug": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/output": "1.54.0", + "@theia/plugin-ext": "1.54.0", + "@theia/workspace": "1.54.0", + "ps-tree": "^1.2.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -48,7 +49,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts b/packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts index fb727aa104941..fd7db87b96583 100644 --- a/packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts +++ b/packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts @@ -248,7 +248,8 @@ export class HostedPluginManagerClient { try { if (this.isDebug) { this.pluginInstanceURL = await this.hostedPluginServer.runDebugHostedPluginInstance(this.pluginLocation!.toString(), { - debugMode: this.hostedPluginPreferences['hosted-plugin.debugMode'] + debugMode: this.hostedPluginPreferences['hosted-plugin.debugMode'], + debugPort: [...this.hostedPluginPreferences['hosted-plugin.debugPorts']] }); await this.startDebugSessionManager(); } else { @@ -372,6 +373,9 @@ export class HostedPluginManagerClient { if (config.pluginLocation) { this.pluginLocation = new URI((!config.pluginLocation.startsWith('/') ? '/' : '') + config.pluginLocation.replace(/\\/g, '/')).withScheme('file'); } + if (config.debugPort === undefined) { + config.debugPort = [...this.hostedPluginPreferences['hosted-plugin.debugPorts']]; + } return config; } diff --git a/packages/plugin-dev/src/browser/hosted-plugin-preferences.ts b/packages/plugin-dev/src/browser/hosted-plugin-preferences.ts index 7143bf40bbc61..01b66cc02c47e 100644 --- a/packages/plugin-dev/src/browser/hosted-plugin-preferences.ts +++ b/packages/plugin-dev/src/browser/hosted-plugin-preferences.ts @@ -17,6 +17,7 @@ import { interfaces } from '@theia/core/shared/inversify'; import { createPreferenceProxy, PreferenceProxy, PreferenceService, PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser'; import { nls } from '@theia/core/lib/common/nls'; +import { PluginDebugPort } from '../common'; export const HostedPluginConfigSchema: PreferenceSchema = { 'type': 'object', @@ -42,6 +43,28 @@ export const HostedPluginConfigSchema: PreferenceSchema = { 'Array of glob patterns for locating generated JavaScript files (`${pluginPath}` will be replaced by plugin actual path).' ), default: ['${pluginPath}/out/**/*.js'] + }, + 'hosted-plugin.debugPorts': { + type: 'array', + items: { + type: 'object', + properties: { + 'serverName': { + type: 'string', + description: nls.localize('theia/plugin-dev/debugPorts/serverName', + 'The plugin host server name, e.g. "hosted-plugin" as in "--hosted-plugin-inspect=" ' + + 'or "headless-hosted-plugin" as in "--headless-hosted-plugin-inspect="'), + }, + 'debugPort': { + type: 'number', + minimum: 0, + maximum: 65535, + description: nls.localize('theia/plugin-dev/debugPorts/debugPort', 'Port to use for this server\'s Node.js debug'), + } + }, + }, + default: undefined, + description: nls.localize('theia/plugin-dev/debugPorts', 'Port configuration per server for Node.js debug'), } } }; @@ -50,6 +73,7 @@ export interface HostedPluginConfiguration { 'hosted-plugin.watchMode': boolean; 'hosted-plugin.debugMode': string; 'hosted-plugin.launchOutFiles': string[]; + 'hosted-plugin.debugPorts': PluginDebugPort[]; } export const HostedPluginPreferenceContribution = Symbol('HostedPluginPreferenceContribution'); diff --git a/packages/plugin-dev/src/common/plugin-dev-protocol.ts b/packages/plugin-dev/src/common/plugin-dev-protocol.ts index c14689928e7e0..9a242ea6026cc 100644 --- a/packages/plugin-dev/src/common/plugin-dev-protocol.ts +++ b/packages/plugin-dev/src/common/plugin-dev-protocol.ts @@ -38,8 +38,13 @@ export interface PluginDevServer extends RpcServer { export interface PluginDevClient { } +export interface PluginDebugPort { + serverName: string, + debugPort: number, +} + export interface PluginDebugConfiguration { debugMode?: string; pluginLocation?: string; - debugPort?: string; + debugPort?: string | PluginDebugPort[] } diff --git a/packages/plugin-dev/src/node/hosted-instance-manager.ts b/packages/plugin-dev/src/node/hosted-instance-manager.ts index dc11d18603478..c8a43d72cadb8 100644 --- a/packages/plugin-dev/src/node/hosted-instance-manager.ts +++ b/packages/plugin-dev/src/node/hosted-instance-manager.ts @@ -24,7 +24,7 @@ import URI from '@theia/core/lib/common/uri'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { HostedPluginUriPostProcessor, HostedPluginUriPostProcessorSymbolName } from './hosted-plugin-uri-postprocessor'; import { environment, isWindows } from '@theia/core'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { LogType } from '@theia/plugin-ext/lib/common/types'; import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin'; import { MetadataScanner } from '@theia/plugin-ext/lib/hosted/node/metadata-scanner'; @@ -266,7 +266,20 @@ export abstract class AbstractHostedInstanceManager implements HostedInstanceMan } if (debugConfig) { - command.push(`--hosted-plugin-${debugConfig.debugMode || 'inspect'}=0.0.0.0${debugConfig.debugPort ? ':' + debugConfig.debugPort : ''}`); + if (debugConfig.debugPort === undefined) { + command.push(`--hosted-plugin-${debugConfig.debugMode || 'inspect'}=0.0.0.0`); + } else if (typeof debugConfig.debugPort === 'string') { + command.push(`--hosted-plugin-${debugConfig.debugMode || 'inspect'}=0.0.0.0:${debugConfig.debugPort}`); + } else if (Array.isArray(debugConfig.debugPort)) { + if (debugConfig.debugPort.length === 0) { + // treat empty array just like undefined + command.push(`--hosted-plugin-${debugConfig.debugMode || 'inspect'}=0.0.0.0`); + } else { + for (const serverToPort of debugConfig.debugPort) { + command.push(`--${serverToPort.serverName}-${debugConfig.debugMode || 'inspect'}=0.0.0.0:${serverToPort.debugPort}`); + } + } + } } return command; } diff --git a/packages/plugin-dev/src/node/hosted-plugin-reader.ts b/packages/plugin-dev/src/node/hosted-plugin-reader.ts index 7aeec23cdeb74..d4bcbf87061c0 100644 --- a/packages/plugin-dev/src/node/hosted-plugin-reader.ts +++ b/packages/plugin-dev/src/node/hosted-plugin-reader.ts @@ -42,7 +42,7 @@ export class HostedPluginReader implements BackendApplicationContribution { const hostedPlugin = new PluginDeployerEntryImpl('Hosted Plugin', pluginPath!, pluginPath); hostedPlugin.storeValue('isUnderDevelopment', true); const hostedMetadata = await this.hostedPlugin.promise; - if (hostedMetadata!.model.entryPoint && hostedMetadata!.model.entryPoint.backend) { + if (hostedMetadata!.model.entryPoint && (hostedMetadata!.model.entryPoint.backend || hostedMetadata!.model.entryPoint.headless)) { this.deployerHandler.deployBackendPlugins([hostedPlugin]); } diff --git a/packages/plugin-ext-headless/.eslintrc.js b/packages/plugin-ext-headless/.eslintrc.js new file mode 100644 index 0000000000000..c452b0b44baec --- /dev/null +++ b/packages/plugin-ext-headless/.eslintrc.js @@ -0,0 +1,13 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json' + }, + rules: { + 'no-null/no-null': 'off', + } +}; diff --git a/packages/plugin-ext-headless/README.md b/packages/plugin-ext-headless/README.md new file mode 100644 index 0000000000000..f77c932c07d09 --- /dev/null +++ b/packages/plugin-ext-headless/README.md @@ -0,0 +1,32 @@ +
    + +
    + +theia-ext-logo + +

    ECLIPSE THEIA - HEADLESS PLUGIN-EXT EXTENSION

    + +
    + +
    + +## Description + +The `@theia/plugin-ext-headless` extension contributes functionality for the backend-only "headless `plugin`" API. +The plugin extension host managed by this extension is scoped to the single Theia NodeJS instance. +This is unlike the plugin extension hosts managed by the [`@theia/plugin-ext` extension][plugin-ext] which are scoped on a per-frontend-connection basis. + +[plugin-ext]: ../plugin-ext/README.md + +## Implementation + +The implementation is derived from the [`@theia/plugin-ext` extension][plugin-ext] for frontend-scoped plugins. + +## 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/plugin-ext-headless/package.json b/packages/plugin-ext-headless/package.json new file mode 100644 index 0000000000000..52ac90e82f9d8 --- /dev/null +++ b/packages/plugin-ext-headless/package.json @@ -0,0 +1,56 @@ +{ + "name": "@theia/plugin-ext-headless", + "version": "1.54.0", + "description": "Theia - Headless (Backend-only) Plugin Extension", + "main": "lib/common/index.js", + "typings": "lib/common/index.d.ts", + "dependencies": { + "@theia/core": "1.54.0", + "@theia/plugin-ext": "1.54.0", + "@theia/terminal": "1.54.0", + "tslib": "^2.6.2" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "backend": "lib/plugin-ext-headless-module", + "backendElectron": "lib/plugin-ext-headless-electron-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", + "@types/decompress": "^4.2.2", + "@types/escape-html": "^0.0.20", + "@types/lodash.clonedeep": "^4.5.3", + "@types/ps-tree": "^1.1.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/plugin-ext-headless/src/common/headless-plugin-container.ts b/packages/plugin-ext-headless/src/common/headless-plugin-container.ts new file mode 100644 index 0000000000000..d7b91419a73a4 --- /dev/null +++ b/packages/plugin-ext-headless/src/common/headless-plugin-container.ts @@ -0,0 +1,23 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/** + * Service identifier for Inversify container modules that are used + * to configure the child Container in which scope the services supporting + * the Headless Plugin Host are isolated from connection-scoped frontend/backend + * plugin hosts and the rest of the Theia Node server. + */ +export const HeadlessPluginContainerModule = Symbol('HeadlessPluginContainerModule'); diff --git a/packages/plugin-ext-headless/src/common/headless-plugin-protocol.ts b/packages/plugin-ext-headless/src/common/headless-plugin-protocol.ts new file mode 100644 index 0000000000000..362ca39d87814 --- /dev/null +++ b/packages/plugin-ext-headless/src/common/headless-plugin-protocol.ts @@ -0,0 +1,38 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export * from '@theia/plugin-ext'; + +declare module '@theia/plugin-ext' { + /** + * Extension of the package manifest interface defined by the core plugin framework. + */ + interface PluginPackage { + /** + * Analogues of declarations offered by VS Code plugins, but for the headless instantiation. + */ + headless?: { + /** Activation events supported in headless mode, if any. */ + activationEvents?: string[]; + } + } +} + +/** + * Name for a `string[]` injection binding contributing headless activation event names + * supported by the application. + */ +export const SupportedHeadlessActivationEvents = Symbol('SupportedHeadlessActivationEvents'); diff --git a/packages/plugin-ext-headless/src/common/headless-plugin-rpc.ts b/packages/plugin-ext-headless/src/common/headless-plugin-rpc.ts new file mode 100644 index 0000000000000..8fd578ffd62d7 --- /dev/null +++ b/packages/plugin-ext-headless/src/common/headless-plugin-rpc.ts @@ -0,0 +1,46 @@ +// ***************************************************************************** +// 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 { AbstractPluginManagerExt, EnvInit } from '@theia/plugin-ext'; +import { KeysToKeysToAnyValue } from '@theia/plugin-ext/lib/common/types'; +import { + MAIN_RPC_CONTEXT, PLUGIN_RPC_CONTEXT +} from '@theia/plugin-ext/lib/common/plugin-api-rpc'; +import { ExtPluginApi } from './plugin-ext-headless-api-contribution'; + +export const HEADLESSPLUGIN_RPC_CONTEXT = { + MESSAGE_REGISTRY_MAIN: PLUGIN_RPC_CONTEXT.MESSAGE_REGISTRY_MAIN, + ENV_MAIN: PLUGIN_RPC_CONTEXT.ENV_MAIN, + NOTIFICATION_MAIN: PLUGIN_RPC_CONTEXT.NOTIFICATION_MAIN, + LOCALIZATION_MAIN: PLUGIN_RPC_CONTEXT.LOCALIZATION_MAIN, +}; + +export const HEADLESSMAIN_RPC_CONTEXT = { + HOSTED_PLUGIN_MANAGER_EXT: createProxyIdentifier('HeadlessPluginManagerExt'), + NOTIFICATION_EXT: MAIN_RPC_CONTEXT.NOTIFICATION_EXT, +}; + +export type HeadlessEnvInit = Pick; + +export interface HeadlessPluginManagerInitializeParams { + activationEvents: string[]; + globalState: KeysToKeysToAnyValue; + env: HeadlessEnvInit; + extApi?: ExtPluginApi[]; +} + +export interface HeadlessPluginManagerExt extends AbstractPluginManagerExt { } diff --git a/packages/plugin-ext-headless/src/common/index.ts b/packages/plugin-ext-headless/src/common/index.ts new file mode 100644 index 0000000000000..279c170ea1f20 --- /dev/null +++ b/packages/plugin-ext-headless/src/common/index.ts @@ -0,0 +1,23 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export * from './headless-plugin-container'; +export { + ExtPluginApi, ExtPluginHeadlessApi, ExtPluginApiProvider, + ExtPluginHeadlessApiProvider +} from './plugin-ext-headless-api-contribution'; +export { PluginPackage, SupportedHeadlessActivationEvents } from './headless-plugin-protocol'; +export * from './headless-plugin-rpc'; diff --git a/packages/plugin-ext-headless/src/common/plugin-ext-headless-api-contribution.ts b/packages/plugin-ext-headless/src/common/plugin-ext-headless-api-contribution.ts new file mode 100644 index 0000000000000..fe9726188f6bb --- /dev/null +++ b/packages/plugin-ext-headless/src/common/plugin-ext-headless-api-contribution.ts @@ -0,0 +1,60 @@ +// ***************************************************************************** +// 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 { PluginManager } from '@theia/plugin-ext'; +import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol'; + +export * from '@theia/plugin-ext'; + +declare module '@theia/plugin-ext' { + /** + * Plugin API extension description. + * This interface describes scripts for all three plugin runtimes: frontend (WebWorker), backend (NodeJs), and headless (NodeJs). + */ + interface ExtPluginApi extends ExtPluginHeadlessApi { + // Note that the frontendInitPath and backendInitPath properties are included by + // Typescript interface merge from the @theia/plugin-ext::ExtPluginApi interface. + } +} + +/** + * Provider for headless extension API description. + */ +export interface ExtPluginHeadlessApiProvider { + /** + * Provide API description. + */ + provideApi(): ExtPluginHeadlessApi; +} + +/** + * Headless Plugin API extension description. + * This interface describes a script for the headless (NodeJs) runtime outside of the scope of frontend connections. + */ +export interface ExtPluginHeadlessApi { + /** + * Path to the script which should be loaded to provide api, module should export `provideApi` function with + * [ExtPluginApiBackendInitializationFn](#ExtPluginApiBackendInitializationFn) signature + */ + headlessInitPath?: string; +} + +/** + * Signature of the extension API initialization function for APIs contributed to headless plugins. + */ +export interface ExtPluginApiHeadlessInitializationFn { + (rpc: RPCProtocol, pluginManager: PluginManager): void; +} diff --git a/packages/plugin-ext-headless/src/hosted/node-electron/plugin-ext-headless-hosted-electron-module.ts b/packages/plugin-ext-headless/src/hosted/node-electron/plugin-ext-headless-hosted-electron-module.ts new file mode 100644 index 0000000000000..64a026d80bf2c --- /dev/null +++ b/packages/plugin-ext-headless/src/hosted/node-electron/plugin-ext-headless-hosted-electron-module.ts @@ -0,0 +1,22 @@ +// ***************************************************************************** +// 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 { interfaces } from '@theia/core/shared/inversify'; +import { bindCommonHostedBackend } from '../node/plugin-ext-headless-hosted-module'; + +export function bindElectronBackend(bind: interfaces.Bind): void { + bindCommonHostedBackend(bind); +} diff --git a/packages/plugin-ext-headless/src/hosted/node/headless-hosted-plugin.ts b/packages/plugin-ext-headless/src/hosted/node/headless-hosted-plugin.ts new file mode 100644 index 0000000000000..f33d583ed2403 --- /dev/null +++ b/packages/plugin-ext-headless/src/hosted/node/headless-hosted-plugin.ts @@ -0,0 +1,199 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// some code copied and modified from https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts + +import { generateUuid } from '@theia/core/lib/common/uuid'; +import { injectable, inject, named } from '@theia/core/shared/inversify'; +import { getPluginId, DeployedPlugin, HostedPluginServer, PluginDeployer } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { setUpPluginApi } from '../../main/node/main-context'; +import { RPCProtocol, RPCProtocolImpl } from '@theia/plugin-ext/lib/common/rpc-protocol'; +import { ContributionProvider, Disposable, DisposableCollection, nls } from '@theia/core'; +import { environment } from '@theia/core/shared/@theia/application-package/lib/environment'; +import { IPCChannel } from '@theia/core/lib/node'; +import { BackendApplicationConfigProvider } from '@theia/core/lib/node/backend-application-config-provider'; +import { HostedPluginProcess } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-process'; +import { IShellTerminalServer } from '@theia/terminal/lib/common/shell-terminal-protocol'; +import { HeadlessPluginManagerExt, HEADLESSMAIN_RPC_CONTEXT } from '../../common/headless-plugin-rpc'; +import { AbstractHostedPluginSupport, PluginContributions } from '@theia/plugin-ext/lib/hosted/common/hosted-plugin'; +import { TheiaHeadlessPluginScanner } from './scanners/scanner-theia-headless'; +import { SupportedHeadlessActivationEvents } from '../../common/headless-plugin-protocol'; +import { PluginDeployerImpl } from '@theia/plugin-ext/lib/main/node/plugin-deployer-impl'; + +import URI from '@theia/core/lib/common/uri'; +import * as fs from 'fs'; +import * as asyncFs from 'fs/promises'; + +export type HeadlessPluginHost = string; + +export function isHeadlessPlugin(plugin: DeployedPlugin): boolean { + return !!plugin.metadata.model.entryPoint.headless; +} + +@injectable() +export class HeadlessHostedPluginSupport extends AbstractHostedPluginSupport { + + @inject(HostedPluginProcess) + protected readonly pluginProcess: HostedPluginProcess; + + @inject(IShellTerminalServer) + protected readonly shellTerminalServer: IShellTerminalServer; + + @inject(TheiaHeadlessPluginScanner) + protected readonly scanner: TheiaHeadlessPluginScanner; + + @inject(PluginDeployer) + protected readonly pluginDeployer: PluginDeployerImpl; + + @inject(ContributionProvider) + @named(SupportedHeadlessActivationEvents) + protected readonly supportedActivationEventsContributions: ContributionProvider; + + constructor() { + super(generateUuid()); + } + + shutDown(): void { + this.pluginProcess.terminatePluginServer(); + } + + protected createTheiaReadyPromise(): Promise { + return Promise.all([this.envServer.getVariables()]); + } + + // Only load headless plugins + protected acceptPlugin(plugin: DeployedPlugin): boolean | DeployedPlugin { + if (!isHeadlessPlugin(plugin)) { + return false; + } + + if (plugin.metadata.model.engine.type === this.scanner.apiType) { + // Easy case: take it as it is + return true; + } + + // Adapt it for headless + return this.scanner.adaptForHeadless(plugin); + } + + protected handleContributions(_plugin: DeployedPlugin): Disposable { + // We have no contribution points, yet, for headless plugins + return Disposable.NULL; + } + + protected override async beforeSyncPlugins(toDisconnect: DisposableCollection): Promise { + await super.beforeSyncPlugins(toDisconnect); + + // Plugin deployment is asynchronous, so wait until that's finished. + return new Promise((resolve, reject) => { + this.pluginDeployer.onDidDeploy(resolve); + toDisconnect.push(Disposable.create(reject)); + }); + } + + protected async obtainManager(host: string, hostContributions: PluginContributions[], toDisconnect: DisposableCollection): Promise { + let manager = this.managers.get(host); + if (!manager) { + const pluginId = getPluginId(hostContributions[0].plugin.metadata.model); + const rpc = this.initRpc(host, pluginId); + toDisconnect.push(rpc); + + manager = rpc.getProxy(HEADLESSMAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); + this.managers.set(host, manager); + toDisconnect.push(Disposable.create(() => this.managers.delete(host))); + + const [extApi, globalState] = await Promise.all([ + this.server.getExtPluginAPI(), + this.pluginServer.getAllStorageValues(undefined) + ]); + if (toDisconnect.disposed) { + return undefined; + } + + const activationEvents = this.supportedActivationEventsContributions.getContributions().flatMap(array => array); + const shell = await this.shellTerminalServer.getDefaultShell(); + const isElectron = environment.electron.is(); + + await manager.$init({ + activationEvents, + globalState, + env: { + language: nls.locale || nls.defaultLocale, + shell, + appName: BackendApplicationConfigProvider.get().applicationName, + appHost: isElectron ? 'desktop' : 'web' // TODO: 'web' could be the embedder's name, e.g. 'github.dev' + }, + extApi + }); + if (toDisconnect.disposed) { + return undefined; + } + } + return manager; + } + + protected initRpc(host: HeadlessPluginHost, pluginId: string): RPCProtocol { + const rpc = this.createServerRpc(host); + this.container.bind(RPCProtocol).toConstantValue(rpc); + setUpPluginApi(rpc, this.container); + this.mainPluginApiProviders.getContributions().forEach(p => p.initialize(rpc, this.container)); + return rpc; + } + + protected createServerRpc(pluginHostId: string): RPCProtocol { + const channel = new IPCChannel(this.pluginProcess['childProcess']); + + return new RPCProtocolImpl(channel); + } + + protected async getStoragePath(): Promise { + // Headless plugins are associated with the main Node process, so + // their storage is the global storage. + return this.getHostGlobalStoragePath(); + } + + protected async getHostGlobalStoragePath(): Promise { + const configDirUri = await this.envServer.getConfigDirUri(); + const globalStorageFolderUri = new URI(configDirUri).resolve('globalStorage'); + const globalStorageFolderUrl = new URL(globalStorageFolderUri.toString()); + + let stat: fs.Stats | undefined; + + try { + stat = await asyncFs.stat(globalStorageFolderUrl); + } catch (_) { + // OK, no such directory + } + + if (stat && !stat.isDirectory()) { + throw new Error(`Global storage folder is not a directory: ${globalStorageFolderUri}`); + } + + // Make sure that folder by the path exists + if (!stat) { + await asyncFs.mkdir(globalStorageFolderUrl, { recursive: true }); + } + + const globalStorageFolderFsPath = await asyncFs.realpath(globalStorageFolderUrl); + if (!globalStorageFolderFsPath) { + throw new Error(`Could not resolve the FS path for URI: ${globalStorageFolderUri}`); + } + return globalStorageFolderFsPath; + } +} diff --git a/packages/plugin-ext-headless/src/hosted/node/headless-plugin-service.ts b/packages/plugin-ext-headless/src/hosted/node/headless-plugin-service.ts new file mode 100644 index 0000000000000..3a36482dedce2 --- /dev/null +++ b/packages/plugin-ext-headless/src/hosted/node/headless-plugin-service.ts @@ -0,0 +1,25 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; +import { HostedPluginServerImpl } from '@theia/plugin-ext/lib/hosted/node/plugin-service'; + +@injectable() +export class HeadlessHostedPluginServerImpl extends HostedPluginServerImpl { + protected override getServerName(): string { + return 'headless-hosted-plugin'; + } +} diff --git a/packages/plugin-ext-headless/src/hosted/node/plugin-ext-headless-hosted-module.ts b/packages/plugin-ext-headless/src/hosted/node/plugin-ext-headless-hosted-module.ts new file mode 100644 index 0000000000000..3e651eee6bedc --- /dev/null +++ b/packages/plugin-ext-headless/src/hosted/node/plugin-ext-headless-hosted-module.ts @@ -0,0 +1,76 @@ +// ***************************************************************************** +// 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 { bindContributionProvider } from '@theia/core/lib/common/contribution-provider'; +import { BackendApplicationContribution } from '@theia/core/lib/node'; +import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; +import { ExtPluginApiProvider, HostedPluginServer, PluginHostEnvironmentVariable, PluginScanner } from '@theia/plugin-ext'; +import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin'; +import { HostedPluginProcess, HostedPluginProcessConfiguration } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-process'; +import { BackendPluginHostableFilter } from '@theia/plugin-ext/lib/hosted/node/plugin-service'; +import { MaybePromise } from '@theia/core'; +import { HeadlessPluginContainerModule } from '../../common/headless-plugin-container'; +import { HeadlessHostedPluginSupport, isHeadlessPlugin } from './headless-hosted-plugin'; +import { TheiaHeadlessPluginScanner } from './scanners/scanner-theia-headless'; +import { SupportedHeadlessActivationEvents } from '../../common/headless-plugin-protocol'; +import { HeadlessHostedPluginServerImpl } from './headless-plugin-service'; + +export function bindCommonHostedBackend(bind: interfaces.Bind): void { + bind(HostedPluginProcess).toSelf().inSingletonScope(); + bind(HostedPluginSupport).toSelf().inSingletonScope(); + + bindContributionProvider(bind, Symbol.for(ExtPluginApiProvider)); + bindContributionProvider(bind, PluginHostEnvironmentVariable); + bindContributionProvider(bind, SupportedHeadlessActivationEvents); + + bind(HeadlessHostedPluginServerImpl).toSelf().inSingletonScope(); + bind(HostedPluginServer).toService(HeadlessHostedPluginServerImpl); + bind(HeadlessHostedPluginSupport).toSelf().inSingletonScope(); + bind(BackendPluginHostableFilter).toConstantValue(isHeadlessPlugin); + + bind(HostedPluginProcessConfiguration).toConstantValue({ + path: path.join(__dirname, 'plugin-host-headless'), + }); +} + +export function bindHeadlessHosted(bind: interfaces.Bind): void { + bind(TheiaHeadlessPluginScanner).toSelf().inSingletonScope(); + bind(PluginScanner).toService(TheiaHeadlessPluginScanner); + bind(SupportedHeadlessActivationEvents).toConstantValue(['*', 'onStartupFinished']); + + bind(BackendApplicationContribution).toDynamicValue(({container}) => { + let hostedPluginSupport: HeadlessHostedPluginSupport | undefined; + + return { + onStart(): MaybePromise { + // Create a child container to isolate the Headless Plugin hosting stack + // from all connection-scoped frontend/backend plugin hosts and + // also to avoid leaking it into the global container scope + const headlessPluginsContainer = container.createChild(); + const modules = container.getAll(HeadlessPluginContainerModule); + headlessPluginsContainer.load(...modules); + + hostedPluginSupport = headlessPluginsContainer.get(HeadlessHostedPluginSupport); + hostedPluginSupport.onStart(headlessPluginsContainer); + }, + + onStop(): void { + hostedPluginSupport?.shutDown(); + } + }; + }); +} diff --git a/packages/plugin-ext-headless/src/hosted/node/plugin-host-headless-module.ts b/packages/plugin-ext-headless/src/hosted/node/plugin-host-headless-module.ts new file mode 100644 index 0000000000000..0237e1662ead9 --- /dev/null +++ b/packages/plugin-ext-headless/src/hosted/node/plugin-host-headless-module.ts @@ -0,0 +1,76 @@ +// ***************************************************************************** +// 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 '@theia/core/shared/reflect-metadata'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { RPCProtocol, RPCProtocolImpl } from '@theia/plugin-ext/lib/common/rpc-protocol'; +import { AbstractPluginHostRPC, PluginContainerModuleLoader } from '@theia/plugin-ext/lib/hosted/node/plugin-host-rpc'; +import { AbstractPluginManagerExtImpl, MinimalTerminalServiceExt } from '@theia/plugin-ext/lib/plugin/plugin-manager'; +import { HeadlessPluginHostRPC } from './plugin-host-headless-rpc'; +import { HeadlessPluginManagerExtImpl } from '../../plugin/headless-plugin-manager'; +import { IPCChannel } from '@theia/core/lib/node'; +import { InternalPluginContainerModule } from '@theia/plugin-ext/lib/plugin/node/plugin-container-module'; + +import { EnvExtImpl } from '@theia/plugin-ext/lib/plugin/env'; +import { EnvNodeExtImpl } from '@theia/plugin-ext/lib/plugin/node/env-node-ext'; +import { LocalizationExt } from '@theia/plugin-ext'; +import { LocalizationExtImpl } from '@theia/plugin-ext/lib/plugin/localization-ext'; +import { InternalStorageExt } from '@theia/plugin-ext/lib/plugin/plugin-storage'; +import { InternalSecretsExt } from '@theia/plugin-ext/lib/plugin/secrets-ext'; +import { EnvironmentVariableCollectionImpl } from '@theia/plugin-ext/lib/plugin/terminal-ext'; +import { Disposable } from '@theia/core'; + +export default new ContainerModule(bind => { + const channel = new IPCChannel(); + bind(RPCProtocol).toConstantValue(new RPCProtocolImpl(channel)); + + bind(PluginContainerModuleLoader).toDynamicValue(({ container }) => + (module: ContainerModule) => { + container.load(module); + const internalModule = module as InternalPluginContainerModule; + const pluginApiCache = internalModule.initializeApi?.(container); + return pluginApiCache; + }).inSingletonScope(); + + bind(AbstractPluginHostRPC).toService(HeadlessPluginHostRPC); + bind(HeadlessPluginHostRPC).toSelf().inSingletonScope(); + bind(AbstractPluginManagerExtImpl).toService(HeadlessPluginManagerExtImpl); + bind(HeadlessPluginManagerExtImpl).toSelf().inSingletonScope(); + bind(EnvExtImpl).to(EnvNodeExtImpl).inSingletonScope(); + bind(LocalizationExt).to(LocalizationExtImpl).inSingletonScope(); + + const dummySecrets: InternalSecretsExt = { + get: () => Promise.resolve(undefined), + store: () => Promise.resolve(undefined), + delete: () => Promise.resolve(undefined), + $onDidChangePassword: () => Promise.resolve(), + onDidChangePassword: () => Disposable.NULL, + }; + const dummyStorage: InternalStorageExt = { + init: () => undefined, + setPerPluginData: () => Promise.resolve(false), + getPerPluginData: () => ({}), + storageDataChangedEvent: () => Disposable.NULL, + $updatePluginsWorkspaceData: () => undefined + }; + const dummyTerminalService: MinimalTerminalServiceExt = { + $initEnvironmentVariableCollections: () => undefined, + $setShell: () => undefined, + getEnvironmentVariableCollection: () => new EnvironmentVariableCollectionImpl(false), + }; + bind(InternalSecretsExt).toConstantValue(dummySecrets); + bind(InternalStorageExt).toConstantValue(dummyStorage); + bind(MinimalTerminalServiceExt).toConstantValue(dummyTerminalService); +}); diff --git a/packages/plugin-ext-headless/src/hosted/node/plugin-host-headless-rpc.ts b/packages/plugin-ext-headless/src/hosted/node/plugin-host-headless-rpc.ts new file mode 100644 index 0000000000000..175fbebcd8ca9 --- /dev/null +++ b/packages/plugin-ext-headless/src/hosted/node/plugin-host-headless-rpc.ts @@ -0,0 +1,80 @@ +// ***************************************************************************** +// 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 { dynamicRequire } from '@theia/core/lib/node/dynamic-require'; +import { ContainerModule, injectable, inject } from '@theia/core/shared/inversify'; +import { EnvExtImpl } from '@theia/plugin-ext/lib/plugin/env'; +import { LocalizationExt } from '@theia/plugin-ext'; +import { LocalizationExtImpl } from '@theia/plugin-ext/lib/plugin/localization-ext'; +import { HEADLESSMAIN_RPC_CONTEXT } from '../../common/headless-plugin-rpc'; +import { HeadlessPluginManagerExtImpl } from '../../plugin/headless-plugin-manager'; +import { AbstractPluginHostRPC, ExtInterfaces } from '@theia/plugin-ext/lib/hosted/node/plugin-host-rpc'; +import { PluginModel } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { ExtPluginApi, ExtPluginApiHeadlessInitializationFn } from '../../common/plugin-ext-headless-api-contribution'; + +type HeadlessExtInterfaces = Pick; + +/** + * The RPC handler for headless plugins. + */ +@injectable() +export class HeadlessPluginHostRPC extends AbstractPluginHostRPC { + @inject(EnvExtImpl) + protected readonly envExt: EnvExtImpl; + + @inject(LocalizationExt) + protected readonly localizationExt: LocalizationExtImpl; + + constructor() { + super('HEADLESS_PLUGIN_HOST', undefined, + { + $pluginManager: HEADLESSMAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, + } + ); + } + + protected createExtInterfaces(): HeadlessExtInterfaces { + return { + envExt: this.envExt, + localizationExt: this.localizationExt + }; + } + + protected createAPIFactory(_extInterfaces: HeadlessExtInterfaces): null { + // As yet there is no default API namespace for backend plugins to access the Theia framework + return null; + } + + protected override getBackendPluginPath(pluginModel: PluginModel): string | undefined { + return pluginModel.entryPoint.headless; + } + + protected initExtApi(extApi: ExtPluginApi): void { + interface PluginExports { + containerModule?: ContainerModule; + provideApi?: ExtPluginApiHeadlessInitializationFn; + } + if (extApi.headlessInitPath) { + const { containerModule, provideApi } = dynamicRequire(extApi.headlessInitPath); + if (containerModule) { + this.loadContainerModule(containerModule); + } + if (provideApi) { + provideApi(this.rpc, this.pluginManager); + } + } + } +} diff --git a/packages/plugin-ext-headless/src/hosted/node/plugin-host-headless.ts b/packages/plugin-ext-headless/src/hosted/node/plugin-host-headless.ts new file mode 100644 index 0000000000000..9d0556db2d635 --- /dev/null +++ b/packages/plugin-ext-headless/src/hosted/node/plugin-host-headless.ts @@ -0,0 +1,111 @@ +// ***************************************************************************** +// Copyright (C) 2018 Red Hat, Inc. 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 '@theia/core/shared/reflect-metadata'; +import { Container } from '@theia/core/shared/inversify'; +import { ConnectionClosedError, RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol'; +import { ProcessTerminatedMessage, ProcessTerminateMessage } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-protocol'; +import { HeadlessPluginHostRPC } from './plugin-host-headless-rpc'; +import pluginHostModule from './plugin-host-headless-module'; + +const banner = `HEADLESS_PLUGIN_HOST(${process.pid}):`; +console.log(banner, 'Starting instance'); + +// override exit() function, to do not allow plugin kill this node +process.exit = function (code?: number): void { + const err = new Error('A plugin called process.exit() but it was blocked.'); + console.warn(banner, err.stack); +} as (code?: number) => never; + +// same for 'crash'(works only in electron) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const proc = process as any; +if (proc.crash) { + proc.crash = function (): void { + const err = new Error('A plugin called process.crash() but it was blocked.'); + console.warn(banner, err.stack); + }; +} + +process.on('uncaughtException', (err: Error) => { + console.error(banner, err); +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const unhandledPromises: Promise[] = []; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +process.on('unhandledRejection', (reason: any, promise: Promise) => { + unhandledPromises.push(promise); + setTimeout(() => { + const index = unhandledPromises.indexOf(promise); + if (index >= 0) { + promise.catch(err => { + unhandledPromises.splice(index, 1); + if (terminating && (ConnectionClosedError.is(err) || ConnectionClosedError.is(reason))) { + // during termination it is expected that pending rpc request are rejected + return; + } + console.error(banner, `Promise rejection not handled in one second: ${err} , reason: ${reason}`); + if (err && err.stack) { + console.error(banner, `With stack trace: ${err.stack}`); + } + }); + } + }, 1000); +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +process.on('rejectionHandled', (promise: Promise) => { + const index = unhandledPromises.indexOf(promise); + if (index >= 0) { + unhandledPromises.splice(index, 1); + } +}); + +let terminating = false; + +const container = new Container(); +container.load(pluginHostModule); + +const rpc: RPCProtocol = container.get(RPCProtocol); +const pluginHostRPC = container.get(HeadlessPluginHostRPC); + +process.on('message', async (message: string) => { + if (terminating) { + return; + } + try { + const msg = JSON.parse(message); + if (ProcessTerminateMessage.is(msg)) { + terminating = true; + if (msg.stopTimeout) { + await Promise.race([ + pluginHostRPC.terminate(), + new Promise(resolve => setTimeout(resolve, msg.stopTimeout)) + ]); + } else { + await pluginHostRPC.terminate(); + } + rpc.dispose(); + if (process.send) { + process.send(JSON.stringify({ type: ProcessTerminatedMessage.TYPE })); + } + + } + } catch (e) { + console.error(banner, e); + } +}); diff --git a/packages/plugin-ext-headless/src/hosted/node/scanners/scanner-theia-headless.ts b/packages/plugin-ext-headless/src/hosted/node/scanners/scanner-theia-headless.ts new file mode 100644 index 0000000000000..58fa0f26837e0 --- /dev/null +++ b/packages/plugin-ext-headless/src/hosted/node/scanners/scanner-theia-headless.ts @@ -0,0 +1,85 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/* eslint-disable @theia/localization-check */ + +import { injectable } from '@theia/core/shared/inversify'; +import { DeployedPlugin, PluginPackage, PluginEntryPoint } from '@theia/plugin-ext'; +import { AbstractPluginScanner } from '@theia/plugin-ext/lib/hosted/node/scanners/scanner-theia'; +import { deepClone } from '@theia/core/lib/common/objects'; + +@injectable() +export class TheiaHeadlessPluginScanner extends AbstractPluginScanner { + + constructor() { + super('theiaHeadlessPlugin'); + } + + protected getEntryPoint(plugin: PluginPackage): PluginEntryPoint { + if (plugin?.theiaPlugin?.headless) { + return { + headless: plugin.theiaPlugin.headless + }; + }; + + return { + headless: plugin.main + }; + } + + /** + * Adapt the given `plugin`'s metadata for headless deployment, where it does not + * already natively specify its headless deployment, such as is the case for plugins + * declaring the `"vscode"` or `"theiaPlugin"` engine. This consists of cloning the + * relevant properties of its deployment metadata and modifying them as required, + * including but not limited to: + * + * - renaming the `lifecycle` start and stop functions as 'activate' and 'deactivate' + * following the VS Code naming convention (in case the `plugin` is a Theia-style + * plugin that uses 'start' and 'stop') + * - deleting inapplicable information such as frontend and backend init script paths + * - filtering/rewriting contributions and/or activation events + * + * The cloning is necessary to retain the original information for the non-headless + * deployments that the plugin also supports. + */ + adaptForHeadless(plugin: DeployedPlugin): DeployedPlugin { + return { + type: plugin.type, + metadata: this.adaptMetadataForHeadless(plugin), + contributes: this.adaptContributesForHeadless(plugin) + }; + } + + protected adaptMetadataForHeadless(plugin: DeployedPlugin): DeployedPlugin['metadata'] { + const result = deepClone(plugin.metadata); + + const lifecycle = result.lifecycle; + delete lifecycle.frontendInitPath; + delete lifecycle.backendInitPath; + + // Same as in VS Code + lifecycle.startMethod = 'activate'; + lifecycle.stopMethod = 'deactivate'; + + return result; + } + + protected adaptContributesForHeadless(plugin: DeployedPlugin): DeployedPlugin['contributes'] { + // We don't yet support and contribution points in headless plugins + return undefined; + } +} diff --git a/packages/plugin-ext-headless/src/index.ts b/packages/plugin-ext-headless/src/index.ts new file mode 100644 index 0000000000000..30d6ae1a4b8a8 --- /dev/null +++ b/packages/plugin-ext-headless/src/index.ts @@ -0,0 +1,17 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export * from './common'; diff --git a/packages/plugin-ext-headless/src/main/node/handlers/plugin-theia-headless-directory-handler.ts b/packages/plugin-ext-headless/src/main/node/handlers/plugin-theia-headless-directory-handler.ts new file mode 100644 index 0000000000000..547d80b5a8364 --- /dev/null +++ b/packages/plugin-ext-headless/src/main/node/handlers/plugin-theia-headless-directory-handler.ts @@ -0,0 +1,35 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; + +import { PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginPackage } from '@theia/plugin-ext'; +import { AbstractPluginDirectoryHandler } from '@theia/plugin-ext/lib/main/node/handlers/plugin-theia-directory-handler'; + +@injectable() +export class PluginTheiaHeadlessDirectoryHandler extends AbstractPluginDirectoryHandler { + + protected acceptManifest(plugin: PluginPackage): boolean { + return plugin?.engines?.theiaPlugin === undefined && 'theiaHeadlessPlugin' in plugin.engines; + } + + async handle(context: PluginDeployerDirectoryHandlerContext): Promise { + await this.copyDirectory(context); + const types: PluginDeployerEntryType[] = [PluginDeployerEntryType.HEADLESS]; + context.pluginEntry().accept(...types); + } + +} diff --git a/packages/plugin-ext-headless/src/main/node/headless-progress-client.ts b/packages/plugin-ext-headless/src/main/node/headless-progress-client.ts new file mode 100644 index 0000000000000..7822402aed6d3 --- /dev/null +++ b/packages/plugin-ext-headless/src/main/node/headless-progress-client.ts @@ -0,0 +1,44 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; +import { + CancellationToken, + ProgressClient, ProgressMessage, ProgressUpdate +} from '@theia/core'; + +/** + * A simple progress client for headless plugins that just writes debug messages to the console + * because there is no one connected frontend to which it is appropriate to send the messages. + */ +@injectable() +export class HeadlessProgressClient implements ProgressClient { + async showProgress(_progressId: string, message: ProgressMessage, cancellationToken: CancellationToken): Promise { + if (cancellationToken.isCancellationRequested) { + return ProgressMessage.Cancel; + } + console.debug(message.text); + } + + async reportProgress(_progressId: string, update: ProgressUpdate, message: ProgressMessage, cancellationToken: CancellationToken): Promise { + if (cancellationToken.isCancellationRequested) { + return; + } + const progress = update.work && update.work.total ? `[${100 * Math.min(update.work.done, update.work.total) / update.work.total}%]` : ''; + const text = `${progress} ${update.message ?? 'completed ...'}`; + console.debug(text); + } +} diff --git a/packages/plugin-ext-headless/src/main/node/main-context.ts b/packages/plugin-ext-headless/src/main/node/main-context.ts new file mode 100644 index 0000000000000..2f848d3be5d48 --- /dev/null +++ b/packages/plugin-ext-headless/src/main/node/main-context.ts @@ -0,0 +1,35 @@ +// ***************************************************************************** +// 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 { interfaces } from '@theia/core/shared/inversify'; +import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol'; +import { EnvMainImpl } from '@theia/plugin-ext/lib/main/common/env-main'; +import { BasicMessageRegistryMainImpl } from '@theia/plugin-ext/lib/main/common/basic-message-registry-main'; +import { BasicNotificationMainImpl } from '@theia/plugin-ext/lib/main/common/basic-notification-main'; + +import { HEADLESSMAIN_RPC_CONTEXT, HEADLESSPLUGIN_RPC_CONTEXT } from '../../common/headless-plugin-rpc'; + +// This sets up only the minimal plugin API required by the plugin manager to report +// messages and notifications to the main side and to initialize plugins. +export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { + const envMain = new EnvMainImpl(rpc, container); + rpc.set(HEADLESSPLUGIN_RPC_CONTEXT.ENV_MAIN, envMain); + + const messageRegistryMain = new BasicMessageRegistryMainImpl(container); + rpc.set(HEADLESSPLUGIN_RPC_CONTEXT.MESSAGE_REGISTRY_MAIN, messageRegistryMain); + + const notificationMain = new BasicNotificationMainImpl(rpc, container, HEADLESSMAIN_RPC_CONTEXT.NOTIFICATION_EXT); + rpc.set(HEADLESSPLUGIN_RPC_CONTEXT.NOTIFICATION_MAIN, notificationMain); +} diff --git a/packages/plugin-ext-headless/src/main/node/plugin-ext-headless-main-module.ts b/packages/plugin-ext-headless/src/main/node/plugin-ext-headless-main-module.ts new file mode 100644 index 0000000000000..19faa51517862 --- /dev/null +++ b/packages/plugin-ext-headless/src/main/node/plugin-ext-headless-main-module.ts @@ -0,0 +1,42 @@ +// ***************************************************************************** +// 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 { interfaces } from '@theia/core/shared/inversify'; +import { + MessageClient, MessageService, + ProgressClient, ProgressService, + bindContributionProvider +} from '@theia/core'; +import { MainPluginApiProvider, PluginDeployerDirectoryHandler } from '@theia/plugin-ext'; +import { PluginTheiaHeadlessDirectoryHandler } from './handlers/plugin-theia-headless-directory-handler'; +import { HeadlessProgressClient } from './headless-progress-client'; + +export function bindHeadlessMain(bind: interfaces.Bind): void { + bind(PluginDeployerDirectoryHandler).to(PluginTheiaHeadlessDirectoryHandler).inSingletonScope(); +} + +export function bindBackendMain(bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind): void { + bindContributionProvider(bind, MainPluginApiProvider); + + // + // Main API dependencies + // + + bind(MessageService).toSelf().inSingletonScope(); + bind(MessageClient).toSelf().inSingletonScope(); // Just logs to console + bind(ProgressService).toSelf().inSingletonScope(); + bind(ProgressClient).to(HeadlessProgressClient).inSingletonScope(); +} diff --git a/packages/plugin-ext-headless/src/package.spec.ts b/packages/plugin-ext-headless/src/package.spec.ts new file mode 100644 index 0000000000000..b918a55863c16 --- /dev/null +++ b/packages/plugin-ext-headless/src/package.spec.ts @@ -0,0 +1,25 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/* + * This is a placeholder for tests that the extension package should implement + * but as yet does not. + * Please delete this file when a real test is implemented. + */ + +describe('plugin-ext-headless package', () => { + it('placeholder to enable mocha', () => true); +}); diff --git a/packages/plugin-ext-headless/src/plugin-ext-headless-electron-module.ts b/packages/plugin-ext-headless/src/plugin-ext-headless-electron-module.ts new file mode 100644 index 0000000000000..489d884e8cdcf --- /dev/null +++ b/packages/plugin-ext-headless/src/plugin-ext-headless-electron-module.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// 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 { HeadlessPluginContainerModule } from './common/headless-plugin-container'; +import { bindElectronBackend } from './hosted/node-electron/plugin-ext-headless-hosted-electron-module'; +import { bindHeadlessMain, bindBackendMain } from './main/node/plugin-ext-headless-main-module'; +import { bindHeadlessHosted } from './hosted/node/plugin-ext-headless-hosted-module'; + +const backendElectronModule = new ContainerModule((bind, unbind, isBound, rebind) => { + bindBackendMain(bind, unbind, isBound, rebind); + bindElectronBackend(bind); +}); + +export default new ContainerModule(bind => { + bind(HeadlessPluginContainerModule).toConstantValue(backendElectronModule); + bindHeadlessMain(bind); + bindHeadlessHosted(bind); +}); diff --git a/packages/plugin-ext-headless/src/plugin-ext-headless-module.ts b/packages/plugin-ext-headless/src/plugin-ext-headless-module.ts new file mode 100644 index 0000000000000..61c0844f6ae0a --- /dev/null +++ b/packages/plugin-ext-headless/src/plugin-ext-headless-module.ts @@ -0,0 +1,31 @@ +// ***************************************************************************** +// 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 { HeadlessPluginContainerModule } from './common/headless-plugin-container'; +import { bindHeadlessHosted, bindCommonHostedBackend } from './hosted/node/plugin-ext-headless-hosted-module'; +import { bindHeadlessMain, bindBackendMain } from './main/node/plugin-ext-headless-main-module'; + +const backendModule = new ContainerModule((bind, unbind, isBound, rebind) => { + bindBackendMain(bind, unbind, isBound, rebind); + bindCommonHostedBackend(bind); +}); + +export default new ContainerModule(bind => { + bind(HeadlessPluginContainerModule).toConstantValue(backendModule); + bindHeadlessMain(bind); + bindHeadlessHosted(bind); +}); diff --git a/packages/plugin-ext-headless/src/plugin/headless-plugin-manager.ts b/packages/plugin-ext-headless/src/plugin/headless-plugin-manager.ts new file mode 100644 index 0000000000000..5f2872f4e9168 --- /dev/null +++ b/packages/plugin-ext-headless/src/plugin/headless-plugin-manager.ts @@ -0,0 +1,50 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; +import { AbstractPluginManagerExtImpl } from '@theia/plugin-ext/lib/plugin/plugin-manager'; +import { HeadlessPluginManagerExt, HeadlessPluginManagerInitializeParams } from '../common/headless-plugin-rpc'; +import { Plugin } from '@theia/plugin-ext'; + +@injectable() +export class HeadlessPluginManagerExtImpl extends AbstractPluginManagerExtImpl implements HeadlessPluginManagerExt { + + private readonly supportedActivationEvents = new Set(); + + async $init(params: HeadlessPluginManagerInitializeParams): Promise { + params.activationEvents?.forEach(event => this.supportedActivationEvents.add(event)); + + this.storage.init(params.globalState, {}); + + this.envExt.setLanguage(params.env.language); + this.envExt.setApplicationName(params.env.appName); + this.envExt.setAppHost(params.env.appHost); + + if (params.extApi) { + this.host.initExtApi(params.extApi); + } + } + + protected override getActivationEvents(plugin: Plugin): string[] | undefined { + const result = plugin.rawModel?.headless?.activationEvents; + return Array.isArray(result) ? result : undefined; + } + + protected isSupportedActivationEvent(activationEvent: string): boolean { + return this.supportedActivationEvents.has(activationEvent.split(':')[0]); + } + +} diff --git a/packages/plugin-ext-headless/tsconfig.json b/packages/plugin-ext-headless/tsconfig.json new file mode 100644 index 0000000000000..e61a769a6936a --- /dev/null +++ b/packages/plugin-ext-headless/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib", + "lib": [ + "es6", + "dom", + "webworker" + ] + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + }, + { + "path": "../plugin-ext" + }, + { + "path": "../terminal" + } + ] +} diff --git a/packages/plugin-ext-vscode/package.json b/packages/plugin-ext-vscode/package.json index 7451191db6c63..b58d98b27c46e 100644 --- a/packages/plugin-ext-vscode/package.json +++ b/packages/plugin-ext-vscode/package.json @@ -1,22 +1,25 @@ { "name": "@theia/plugin-ext-vscode", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Plugin Extension for VsCode", "dependencies": { - "@theia/callhierarchy": "1.44.0", - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/monaco": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/navigator": "1.44.0", - "@theia/plugin": "1.44.0", - "@theia/plugin-ext": "1.44.0", - "@theia/terminal": "1.44.0", - "@theia/typehierarchy": "1.44.0", - "@theia/userstorage": "1.44.0", - "@theia/workspace": "1.44.0", - "filenamify": "^4.1.0" + "@theia/callhierarchy": "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/navigator": "1.54.0", + "@theia/outline-view": "1.54.0", + "@theia/plugin": "1.54.0", + "@theia/plugin-ext": "1.54.0", + "@theia/terminal": "1.54.0", + "@theia/typehierarchy": "1.54.0", + "@theia/userstorage": "1.54.0", + "@theia/workspace": "1.54.0", + "decompress": "^4.2.1", + "filenamify": "^4.1.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -52,7 +55,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts index 8c487c5c064c1..ce02e28545981 100755 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts @@ -44,7 +44,7 @@ import { DocumentHighlight } from '@theia/plugin-ext/lib/common/plugin-api-rpc-model'; import { DocumentsMainImpl } from '@theia/plugin-ext/lib/main/browser/documents-main'; -import { isUriComponents, toDocumentSymbol, toPosition } from '@theia/plugin-ext/lib/plugin/type-converters'; +import { isUriComponents, toMergedSymbol, toPosition } from '@theia/plugin-ext/lib/plugin/type-converters'; import { ViewColumn } from '@theia/plugin-ext/lib/plugin/types-impl'; import { WorkspaceCommands } from '@theia/workspace/lib/browser'; import { WorkspaceService, WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service'; @@ -79,8 +79,17 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service'; import * as monaco from '@theia/monaco-editor-core'; import { VSCodeExtensionUri } from '../common/plugin-vscode-uri'; import { CodeEditorWidgetUtil } from '@theia/plugin-ext/lib/main/browser/menus/vscode-theia-menu-mappings'; +import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution'; +import { Range } from '@theia/plugin'; +import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages'; export namespace VscodeCommands { + + export const GET_CODE_EXCHANGE_ENDPOINTS: Command = { + id: 'workbench.getCodeExchangeProxyEndpoints' // this command is used in the github auth built-in + // see: https://github.com/microsoft/vscode/blob/191be39e5ac872e03f9d79cc859d9917f40ad935/extensions/github-authentication/src/githubServer.ts#L60 + }; + export const OPEN: Command = { id: 'vscode.open' }; @@ -180,6 +189,10 @@ export class PluginVscodeCommandsContribution implements CommandContribution { protected readonly windowService: WindowService; @inject(MessageService) protected readonly messageService: MessageService; + @inject(OutlineViewContribution) + protected outlineViewContribution: OutlineViewContribution; + @inject(MonacoLanguages) + protected monacoLanguages: MonacoLanguages; private async openWith(commandId: string, resource: URI, columnOrOptions?: ViewColumn | TextDocumentShowOptions, openerId?: string): Promise { if (!resource) { @@ -227,6 +240,10 @@ export class PluginVscodeCommandsContribution implements CommandContribution { } registerCommands(commands: CommandRegistry): void { + commands.registerCommand(VscodeCommands.GET_CODE_EXCHANGE_ENDPOINTS, { + execute: () => undefined // this is a dummy implementation: only used in the case of web apps, which is not supported yet. + }); + commands.registerCommand(VscodeCommands.OPEN, { isVisible: () => false, execute: async (resource: URI | string, columnOrOptions?: ViewColumn | TextDocumentShowOptions) => { @@ -356,7 +373,7 @@ export class PluginVscodeCommandsContribution implements CommandContribution { commands.registerCommand({ id: VscodeCommands.INSTALL_FROM_VSIX.id }, { execute: async (vsixUriOrExtensionId: TheiaURI | UriComponents | string) => { if (typeof vsixUriOrExtensionId === 'string') { - await this.pluginServer.deploy(VSCodeExtensionUri.toVsxExtensionUriString(vsixUriOrExtensionId)); + await this.pluginServer.deploy(VSCodeExtensionUri.fromId(vsixUriOrExtensionId).toString()); } else { const uriPath = isUriComponents(vsixUriOrExtensionId) ? URI.revive(vsixUriOrExtensionId).fsPath : await this.fileService.fsPath(vsixUriOrExtensionId); await this.pluginServer.deploy(`local-file:${uriPath}`); @@ -611,7 +628,7 @@ export class PluginVscodeCommandsContribution implements CommandContribution { if (!Array.isArray(value) || value === undefined) { return undefined; } - return value.map(loc => toDocumentSymbol(loc)); + return value.map(loc => toMergedSymbol(resource, loc)); }) } ); @@ -642,6 +659,38 @@ export class PluginVscodeCommandsContribution implements CommandContribution { commands.executeCommand('_executeFormatOnTypeProvider', monaco.Uri.from(resource), position, ch, options)) } ); + commands.registerCommand( + { + id: 'vscode.executeFoldingRangeProvider' + }, + { + execute: ((resource: URI, position: Position) => + commands.executeCommand('_executeFoldingRangeProvider', monaco.Uri.from(resource), position)) + } + ); + commands.registerCommand( + { + id: 'vscode.executeCodeActionProvider' + }, + { + execute: ((resource: URI, range: Range, kind?: string, itemResolveCount?: number) => + commands.executeCommand('_executeCodeActionProvider', monaco.Uri.from(resource), range, kind, itemResolveCount)) + } + ); + commands.registerCommand( + { + id: 'vscode.executeWorkspaceSymbolProvider' + }, + { + execute: async (queryString: string) => + (await Promise.all( + this.monacoLanguages.workspaceSymbolProviders + .map(async provider => provider.provideWorkspaceSymbols({ query: queryString }, new CancellationTokenSource().token)))) + .flatMap(symbols => symbols) + .filter(symbols => !!symbols) + } + ); + commands.registerCommand( { id: 'vscode.prepareCallHierarchy' @@ -912,6 +961,11 @@ export class PluginVscodeCommandsContribution implements CommandContribution { }; } }); + + // required by Jupyter for the show table of contents action + commands.registerCommand({ id: 'outline.focus' }, { + execute: () => this.outlineViewContribution.openView({ activate: true }) + }); } private async resolveLanguageId(resource: URI): Promise { diff --git a/packages/plugin-ext-vscode/src/common/plugin-vscode-environment.ts b/packages/plugin-ext-vscode/src/common/plugin-vscode-environment.ts index 16601eac78174..6e534f81c245b 100644 --- a/packages/plugin-ext-vscode/src/common/plugin-vscode-environment.ts +++ b/packages/plugin-ext-vscode/src/common/plugin-vscode-environment.ts @@ -24,13 +24,36 @@ export class PluginVSCodeEnvironment { @inject(EnvVariablesServer) protected readonly environments: EnvVariablesServer; - protected _extensionsDirUri: URI | undefined; - async getExtensionsDirUri(): Promise { - if (!this._extensionsDirUri) { + protected _userExtensionsDirUri: URI | undefined; + protected _deployedPluginsUri: URI | undefined; + protected _tmpDirUri: URI | undefined; + + async getUserExtensionsDirUri(): Promise { + if (!this._userExtensionsDirUri) { + const configDir = new URI(await this.environments.getConfigDirUri()); + this._userExtensionsDirUri = configDir.resolve('extensions'); + } + return this._userExtensionsDirUri; + } + + async getDeploymentDirUri(): Promise { + if (!this._deployedPluginsUri) { const configDir = new URI(await this.environments.getConfigDirUri()); - this._extensionsDirUri = configDir.resolve('extensions'); + this._deployedPluginsUri = configDir.resolve('deployedPlugins'); } - return this._extensionsDirUri; + return this._deployedPluginsUri; } + async getTempDirUri(prefix?: string): Promise { + if (!this._tmpDirUri) { + const configDir: URI = new URI(await this.environments.getConfigDirUri()); + this._tmpDirUri = configDir.resolve('tmp'); + } + + if (prefix) { + return this._tmpDirUri.resolve(prefix); + } + + return this._tmpDirUri; + } } diff --git a/packages/plugin-ext-vscode/src/common/plugin-vscode-uri.ts b/packages/plugin-ext-vscode/src/common/plugin-vscode-uri.ts index 116dc80cc0858..b0c0acec9390d 100644 --- a/packages/plugin-ext-vscode/src/common/plugin-vscode-uri.ts +++ b/packages/plugin-ext-vscode/src/common/plugin-vscode-uri.ts @@ -21,26 +21,24 @@ import URI from '@theia/core/lib/common/uri'; * In practice, this means that it will be resolved and deployed by the Open-VSX system. */ export namespace VSCodeExtensionUri { - export const VSCODE_PREFIX = 'vscode:extension/'; - /** - * Should be used to prefix a plugin's ID to ensure that it is identified as a VSX Extension. - * @returns `vscode:extension/${id}` - */ - export function toVsxExtensionUriString(id: string): string { - return `${VSCODE_PREFIX}${id}`; - } - export function toUri(name: string, namespace: string): URI; - export function toUri(id: string): URI; - export function toUri(idOrName: string, namespace?: string): URI { - if (typeof namespace === 'string') { - return new URI(toVsxExtensionUriString(`${namespace}.${idOrName}`)); + export const SCHEME = 'vscode-extension'; + + export function fromId(id: string, version?: string): URI { + if (typeof version === 'string') { + return new URI().withScheme(VSCodeExtensionUri.SCHEME).withAuthority(id).withPath(`/${version}`); } else { - return new URI(toVsxExtensionUriString(idOrName)); + return new URI().withScheme(VSCodeExtensionUri.SCHEME).withAuthority(id); } } - export function toId(uri: URI): string | undefined { - if (uri.scheme === 'vscode' && uri.path.dir.toString() === 'extension') { - return uri.path.base; + + export function fromVersionedId(versionedId: string): URI { + const versionAndId = versionedId.split('@'); + return fromId(versionAndId[0], versionAndId[1]); + } + + export function toId(uri: URI): { id: string, version?: string } | undefined { + if (uri.scheme === VSCodeExtensionUri.SCHEME) { + return { id: uri.authority, version: uri.path.isRoot ? undefined : uri.path.base }; } return undefined; } diff --git a/packages/plugin-ext-vscode/src/node/local-vsix-file-plugin-deployer-resolver.ts b/packages/plugin-ext-vscode/src/node/local-vsix-file-plugin-deployer-resolver.ts index a151ea26395bf..5de67f2206fbb 100644 --- a/packages/plugin-ext-vscode/src/node/local-vsix-file-plugin-deployer-resolver.ts +++ b/packages/plugin-ext-vscode/src/node/local-vsix-file-plugin-deployer-resolver.ts @@ -14,18 +14,18 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import * as fs from '@theia/core/shared/fs-extra'; import * as path from 'path'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { FileUri } from '@theia/core/lib/node'; import { PluginDeployerResolverContext } from '@theia/plugin-ext'; import { LocalPluginDeployerResolver } from '@theia/plugin-ext/lib/main/node/resolvers/local-plugin-deployer-resolver'; import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; import { isVSCodePluginFile } from './plugin-vscode-file-handler'; +import { existsInDeploymentDir, unpackToDeploymentDir } from './plugin-vscode-utils'; @injectable() export class LocalVSIXFilePluginDeployerResolver extends LocalPluginDeployerResolver { static LOCAL_FILE = 'local-file'; + static FILE_EXTENSION = '.vsix'; @inject(PluginVSCodeEnvironment) protected readonly environment: PluginVSCodeEnvironment; @@ -38,28 +38,14 @@ export class LocalVSIXFilePluginDeployerResolver extends LocalPluginDeployerReso } async resolveFromLocalPath(pluginResolverContext: PluginDeployerResolverContext, localPath: string): Promise { - const fileName = path.basename(localPath); - const pathInUserExtensionsDirectory = await this.ensureDiscoverability(localPath); - pluginResolverContext.addPlugin(fileName, pathInUserExtensionsDirectory); - } + const extensionId = path.basename(localPath, LocalVSIXFilePluginDeployerResolver.FILE_EXTENSION); - /** - * Ensures that a user-installed plugin file is transferred to the user extension folder. - */ - protected async ensureDiscoverability(localPath: string): Promise { - const userExtensionsDir = await this.environment.getExtensionsDirUri(); - if (!userExtensionsDir.isEqualOrParent(FileUri.create(localPath))) { - try { - const newPath = FileUri.fsPath(userExtensionsDir.resolve(path.basename(localPath))); - await fs.mkdirp(FileUri.fsPath(userExtensionsDir)); - await new Promise((resolve, reject) => { - fs.copyFile(localPath, newPath, error => error ? reject(error) : resolve()); - }); - return newPath; - } catch (e) { - console.warn(`Problem copying plugin at ${localPath}:`, e); - } + if (await existsInDeploymentDir(this.environment, extensionId)) { + console.log(`[${pluginResolverContext.getOriginId()}]: Target dir already exists in plugin deployment dir`); + return; } - return localPath; + + const extensionDeploymentDir = await unpackToDeploymentDir(this.environment, localPath, extensionId); + pluginResolverContext.addPlugin(extensionId, extensionDeploymentDir); } } diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-deployer-participant.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-deployer-participant.ts index 5508feda6d79f..aefa68b3f7b5b 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-deployer-participant.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-deployer-participant.ts @@ -15,8 +15,11 @@ // ***************************************************************************** import { injectable, inject } from '@theia/core/shared/inversify'; +import * as fs from '@theia/core/shared/fs-extra'; +import { FileUri } from '@theia/core/lib/node'; import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; import { PluginDeployerParticipant, PluginDeployerStartContext } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { LocalVSIXFilePluginDeployerResolver } from './local-vsix-file-plugin-deployer-resolver'; @injectable() export class PluginVSCodeDeployerParticipant implements PluginDeployerParticipant { @@ -25,8 +28,21 @@ export class PluginVSCodeDeployerParticipant implements PluginDeployerParticipan protected readonly environments: PluginVSCodeEnvironment; async onWillStart(context: PluginDeployerStartContext): Promise { - const extensionsDirUri = await this.environments.getExtensionsDirUri(); - context.userEntries.push(extensionsDirUri.withScheme('local-dir').toString()); - } + const extensionDeploymentDirUri = await this.environments.getDeploymentDirUri(); + context.userEntries.push(extensionDeploymentDirUri.withScheme('local-dir').toString()); + + const userExtensionDirUri = await this.environments.getUserExtensionsDirUri(); + const userExtensionDirPath = FileUri.fsPath(userExtensionDirUri); + if (await fs.pathExists(userExtensionDirPath)) { + const files = await fs.readdir(userExtensionDirPath); + for (const file of files) { + if (file.endsWith(LocalVSIXFilePluginDeployerResolver.FILE_EXTENSION)) { + const extensionUri = userExtensionDirUri.resolve(file).withScheme('local-file').toString(); + console.log(`found drop-in extension "${extensionUri}"`); + context.userEntries.push(extensionUri); + } + } + } + } } diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts index f135aead0413e..bcbce5d3fc0cb 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts @@ -15,18 +15,16 @@ // ***************************************************************************** import * as path from 'path'; -import * as filenamify from 'filenamify'; import * as fs from '@theia/core/shared/fs-extra'; import { inject, injectable } from '@theia/core/shared/inversify'; import type { RecursivePartial, URI } from '@theia/core'; import { Deferred, firstTrue } from '@theia/core/lib/common/promise-util'; -import { getTempDirPathAsync } from '@theia/plugin-ext/lib/main/node/temp-dir-util'; import { PluginDeployerDirectoryHandler, PluginDeployerEntry, PluginDeployerDirectoryHandlerContext, - PluginDeployerEntryType, PluginPackage, PluginType, PluginIdentifiers + PluginDeployerEntryType, PluginPackage, PluginIdentifiers } from '@theia/plugin-ext'; -import { FileUri } from '@theia/core/lib/node'; import { PluginCliContribution } from '@theia/plugin-ext/lib/main/node/plugin-cli-contribution'; +import { TMP_DIR_PREFIX } from './plugin-vscode-utils'; @injectable() export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHandler { @@ -35,14 +33,12 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand @inject(PluginCliContribution) protected readonly pluginCli: PluginCliContribution; - constructor() { - this.deploymentDirectory = new Deferred(); - getTempDirPathAsync('vscode-copied') - .then(deploymentDirectoryPath => this.deploymentDirectory.resolve(FileUri.create(deploymentDirectoryPath))); - } - async accept(plugin: PluginDeployerEntry): Promise { console.debug(`Resolving "${plugin.id()}" as a VS Code extension...`); + if (plugin.path().startsWith(TMP_DIR_PREFIX)) { + // avoid adding corrupted plugins from temporary directories + return false; + } return this.attemptResolution(plugin); } @@ -62,7 +58,6 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand } async handle(context: PluginDeployerDirectoryHandlerContext): Promise { - await this.copyDirectory(context); const types: PluginDeployerEntryType[] = []; const packageJson: PluginPackage = context.pluginEntry().getValue('package.json'); if (packageJson.browser) { @@ -74,33 +69,6 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand context.pluginEntry().accept(...types); } - protected async copyDirectory(context: PluginDeployerDirectoryHandlerContext): Promise { - if (this.pluginCli.copyUncompressedPlugins() && context.pluginEntry().type === PluginType.User) { - const entry = context.pluginEntry(); - const id = entry.id(); - const pathToRestore = entry.path(); - const origin = entry.originalPath(); - const targetDir = await this.getExtensionDir(context); - try { - if (await fs.pathExists(targetDir) || !entry.path().startsWith(origin)) { - console.log(`[${id}]: already copied.`); - } else { - console.log(`[${id}]: copying to "${targetDir}"`); - const deploymentDirectory = await this.deploymentDirectory.promise; - await fs.mkdirp(FileUri.fsPath(deploymentDirectory)); - await context.copy(origin, targetDir); - entry.updatePath(targetDir); - if (!this.deriveMetadata(entry)) { - throw new Error('Unable to resolve plugin metadata after copying'); - } - } - } catch (e) { - console.warn(`[${id}]: Error when copying.`, e); - entry.updatePath(pathToRestore); - } - } - } - protected async resolveFromSources(plugin: PluginDeployerEntry): Promise { const pluginPath = plugin.path(); const pck = await this.requirePackage(pluginPath); @@ -152,9 +120,4 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand return undefined; } } - - protected async getExtensionDir(context: PluginDeployerDirectoryHandlerContext): Promise { - const deploymentDirectory = await this.deploymentDirectory.promise; - return FileUri.fsPath(deploymentDirectory.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); - } } diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts index 23e3e3e9292d3..599d61fae2158 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts @@ -14,33 +14,21 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext, PluginType } from '@theia/plugin-ext'; -import * as fs from '@theia/core/shared/fs-extra'; -import * as path from 'path'; +import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext } from '@theia/plugin-ext'; import * as filenamify from 'filenamify'; -import type { URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { Deferred } from '@theia/core/lib/common/promise-util'; -import { getTempDirPathAsync } from '@theia/plugin-ext/lib/main/node/temp-dir-util'; +import * as fs from '@theia/core/shared/fs-extra'; import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; +import { unpackToDeploymentDir } from './plugin-vscode-utils'; export const isVSCodePluginFile = (pluginPath?: string) => Boolean(pluginPath && (pluginPath.endsWith('.vsix') || pluginPath.endsWith('.tgz'))); @injectable() export class PluginVsCodeFileHandler implements PluginDeployerFileHandler { - @inject(PluginVSCodeEnvironment) protected readonly environment: PluginVSCodeEnvironment; - private readonly systemExtensionsDirUri: Deferred; - - constructor() { - this.systemExtensionsDirUri = new Deferred(); - getTempDirPathAsync('vscode-unpacked') - .then(systemExtensionsDirPath => this.systemExtensionsDirUri.resolve(FileUri.create(systemExtensionsDirPath))); - } - async accept(resolvedPlugin: PluginDeployerEntry): Promise { return resolvedPlugin.isFile().then(file => { if (!file) { @@ -51,33 +39,24 @@ export class PluginVsCodeFileHandler implements PluginDeployerFileHandler { } async handle(context: PluginDeployerFileHandlerContext): Promise { - const id = context.pluginEntry().id(); - const extensionDir = await this.getExtensionDir(context); - console.log(`[${id}]: trying to decompress into "${extensionDir}"...`); - if (context.pluginEntry().type === PluginType.User && await fs.pathExists(extensionDir)) { - console.log(`[${id}]: already found`); - context.pluginEntry().updatePath(extensionDir); - return; - } - await this.decompress(extensionDir, context); - console.log(`[${id}]: decompressed`); - context.pluginEntry().updatePath(extensionDir); - } - - protected async getExtensionDir(context: PluginDeployerFileHandlerContext): Promise { - const systemExtensionsDirUri = await this.systemExtensionsDirUri.promise; - return FileUri.fsPath(systemExtensionsDirUri.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); - } - - protected async decompress(extensionDir: string, context: PluginDeployerFileHandlerContext): Promise { - await context.unzip(context.pluginEntry().path(), extensionDir); - if (context.pluginEntry().path().endsWith('.tgz')) { - const extensionPath = path.join(extensionDir, 'package'); - const vscodeNodeModulesPath = path.join(extensionPath, 'vscode_node_modules.zip'); - if (await fs.pathExists(vscodeNodeModulesPath)) { - await context.unzip(vscodeNodeModulesPath, path.join(extensionPath, 'node_modules')); + const id = this.getNormalizedExtensionId(context.pluginEntry().id()); + const extensionDeploymentDir = await unpackToDeploymentDir(this.environment, context.pluginEntry().path(), id); + context.pluginEntry().updatePath(extensionDeploymentDir); + console.log(`root path: ${context.pluginEntry().rootPath}`); + const originalPath = context.pluginEntry().originalPath(); + if (originalPath && originalPath !== extensionDeploymentDir) { + const tempDirUri = await this.environment.getTempDirUri(); + if (originalPath.startsWith(FileUri.fsPath(tempDirUri))) { + try { + await fs.remove(FileUri.fsPath(originalPath)); + } catch (e) { + console.error(`[${id}]: failed to remove temporary files: "${originalPath}"`, e); + } } } } + protected getNormalizedExtensionId(pluginId: string): string { + return filenamify(pluginId, { replacement: '_' }).replace(/\.vsix$/, ''); + } } diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-utils.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-utils.ts new file mode 100644 index 0000000000000..6f6b7d6c6d60c --- /dev/null +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-utils.ts @@ -0,0 +1,101 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 * as decompress from 'decompress'; +import * as path from 'path'; +import * as filenamify from 'filenamify'; +import { FileUri } from '@theia/core/lib/node'; +import * as fs from '@theia/core/shared/fs-extra'; +import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; + +export async function decompressExtension(sourcePath: string, destPath: string): Promise { + try { + await decompress(sourcePath, destPath); + if (sourcePath.endsWith('.tgz')) { + // unzip node_modules from built-in extensions, see https://github.com/eclipse-theia/theia/issues/5756 + const extensionPath = path.join(destPath, 'package'); + const vscodeNodeModulesPath = path.join(extensionPath, 'vscode_node_modules.zip'); + if (await fs.pathExists(vscodeNodeModulesPath)) { + await decompress(vscodeNodeModulesPath, path.join(extensionPath, 'node_modules')); + } + } + return true; + } catch (error) { + console.error(`Failed to decompress ${sourcePath} to ${destPath}: ${error}`); + throw error; + } +} + +export async function existsInDeploymentDir(env: PluginVSCodeEnvironment, extensionId: string): Promise { + return fs.pathExists(await getExtensionDeploymentDir(env, extensionId)); +} + +export const TMP_DIR_PREFIX = 'tmp-vscode-unpacked-'; +export async function unpackToDeploymentDir(env: PluginVSCodeEnvironment, sourcePath: string, extensionId: string): Promise { + const extensionDeploymentDir = await getExtensionDeploymentDir(env, extensionId); + if (await fs.pathExists(extensionDeploymentDir)) { + console.log(`[${extensionId}]: deployment dir "${extensionDeploymentDir}" already exists`); + return extensionDeploymentDir; + } + + const tempDir = await getTempDir(env, TMP_DIR_PREFIX); + try { + console.log(`[${extensionId}]: trying to decompress "${sourcePath}" into "${tempDir}"...`); + if (!await decompressExtension(sourcePath, tempDir)) { + await fs.remove(tempDir); + const msg = `[${extensionId}]: decompressing "${sourcePath}" to "${tempDir}" failed`; + console.error(msg); + throw new Error(msg); + } + } catch (e) { + await fs.remove(tempDir); + const msg = `[${extensionId}]: error while decompressing "${sourcePath}" to "${tempDir}"`; + console.error(msg, e); + throw e; + } + console.log(`[${extensionId}]: decompressed to temp dir "${tempDir}"`); + + try { + console.log(`[${extensionId}]: renaming to extension dir "${extensionDeploymentDir}"...`); + await fs.rename(tempDir, extensionDeploymentDir); + return extensionDeploymentDir; + } catch (e) { + await fs.remove(tempDir); + console.error(`[${extensionId}]: error while renaming "${tempDir}" to "${extensionDeploymentDir}"`, e); + throw e; + } +} + +export async function getExtensionDeploymentDir(env: PluginVSCodeEnvironment, extensionId: string): Promise { + const deployedPluginsDirUri = await env.getDeploymentDirUri(); + const normalizedExtensionId = filenamify(extensionId, { replacement: '_' }); + const extensionDeploymentDirPath = FileUri.fsPath(deployedPluginsDirUri.resolve(normalizedExtensionId)); + return extensionDeploymentDirPath; +} + +export async function getTempDir(env: PluginVSCodeEnvironment, prefix: string): Promise { + const deploymentDirPath = FileUri.fsPath(await env.getDeploymentDirUri()); + try { + if (!await fs.pathExists(deploymentDirPath)) { + console.log(`Creating deployment dir ${deploymentDirPath}`); + await fs.mkdirs(deploymentDirPath); + } + return await fs.mkdtemp(path.join(deploymentDirPath, prefix)); + } catch (error) { + console.error(`Failed to create deployment dir ${deploymentDirPath}: ${error}`); + throw error; + } +} diff --git a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts index 7b1049eea1e5d..705dc25fad516 100644 --- a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts +++ b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts @@ -51,6 +51,10 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca // Default to using backend entryPoint.backend = plugin.main; } + if (plugin.theiaPlugin?.headless) { + // Support the Theia-specific extension for headless plugins + entryPoint.headless = plugin.theiaPlugin?.headless; + } const result: PluginModel = { packagePath: plugin.packagePath, @@ -87,7 +91,7 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca // Iterate over the list of dependencies present, and add them to the collection. dependency.forEach((dep: string) => { const dependencyId = dep.toLowerCase(); - dependencies.set(dependencyId, VSCodeExtensionUri.toVsxExtensionUriString(dependencyId)); + dependencies.set(dependencyId, VSCodeExtensionUri.fromId(dependencyId).toString()); }); } } diff --git a/packages/plugin-ext-vscode/tsconfig.json b/packages/plugin-ext-vscode/tsconfig.json index deb8724d9036a..c1cb4d0e2f0d2 100644 --- a/packages/plugin-ext-vscode/tsconfig.json +++ b/packages/plugin-ext-vscode/tsconfig.json @@ -32,6 +32,9 @@ { "path": "../navigator" }, + { + "path": "../outline-view" + }, { "path": "../plugin" }, diff --git a/packages/plugin-ext/doc/how-to-add-new-custom-plugin-api.md b/packages/plugin-ext/doc/how-to-add-new-custom-plugin-api.md new file mode 100644 index 0000000000000..c19c68bbefb1e --- /dev/null +++ b/packages/plugin-ext/doc/how-to-add-new-custom-plugin-api.md @@ -0,0 +1,290 @@ +# How to add new custom plugin API + +As a Theia developer, you might want to make your app extensible by plugins in ways that are unique to your application. +That will require API that goes beyond what's in the VS Code Extension API and the Theia plugin API. +You can do that by implementing a Theia extension that creates and exposes an API object within the plugin host. +The API object can be imported by your plugins and exposes one or more API namespaces. + +Depending on the plugin host we can either provide a frontend or backend plugin API, or an API for headless plugins that extend or otherwise access backend services: + +- In the backend plugin host that runs in the Node environment in a separate process, we adapt the module loading to return a custom API object instead of loading a module with a particular name. +There is a distinct plugin host for each connected Theia frontend. +- In the frontend plugin host that runs in the browser environment via a web worker, we import the API scripts and put it in the global context. +There is a distinct plugin host for each connected Theia frontend. +- In the headless plugin host that also runs in the Node environment in a separate process, we similarly adapt the module loading mechanism. +When the first headless plugin is deployed, whether at start-up or upon later installation during run-time, then the one and only headless plugin host process is started. + +In this document we focus on the implementation of a custom backend plugin API. +Headless plugin APIs are similar, and the same API can be contributed to both backend and headless plugin hosts. +All three APIs — backend, frontend, and headless — can be provided by implementing and binding an `ExtPluginApiProvider` which should be packaged as a Theia extension. + +## Declare your plugin API provider + +The plugin API provider is executed on the respective plugin host to add your custom API object and namespaces. +Add `@theia/plugin-ext` as a dependency in your `package.json`. +If your plugin is contributing API to headless plugins, then you also need to add the `@theia/plugin-ext-headless` package as a dependency. + +Example Foo Plugin API provider. +Here we see that it provides the same API initialized by the same script to both backend plugins that are frontend-connection-scoped and to headless plugins. +Any combination of these API initialization scripts may be provided, offering the same or differing capabilities in each respective plugin host, although of course it would be odd to provide API to none of them. + +```typescript +@injectable() +export class FooExtPluginApiProvider implements ExtPluginApiProvider { + provideApi(): ExtPluginApi { + return { + frontendExtApi: { + initPath: '/path/to/foo/api/implementation.js', + initFunction: 'fooInitializationFunction', + initVariable: 'foo_global_variable' + }, + backendInitPath: path.join(__dirname, 'foo-init'), + // Provide the same API to headless plugins, too (or a different/subset API) + headlessInitPath: path.join(__dirname, 'foo-init') + }; + } +} +``` + +Register your Plugin API provider in a backend module: + +```typescript + bind(FooExtPluginApiProvider).toSelf().inSingletonScope(); + bind(Symbol.for(ExtPluginApiProvider)).toService(FooExtPluginApiProvider); +``` + +## Define your API + +To ease the usage of your API, it should be developed as separate npm package that can be easily imported without any additional dependencies, cf, the VS Code API or the Theia Plugin API. + +Example `foo.d.ts`: + +```typescript +declare module '@bar/foo' { + export class Foo { } + + export namespace fooBar { + export function getFoo(): Promise; + } +} +``` + +## Implement your plugin API provider + +In our example, we aim to provide a new API object for the backend. +Theia expects that the `backendInitPath` or `headlessInitPath` that we specified in our API provider exports an [InversifyJS](https://inversify.io) `ContainerModule` under the name `containerModule`. +This container-module configures the Inversify `Container` in the plugin host for creation of our API object. +It also implements for us the customization of Node's module loading system to hook our API factory into the import of the module name that we choose. + +Example `node/foo-init.ts`: + +```typescript +import { inject, injectable } 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 { PluginContainerModule } from '@theia/plugin-ext/lib/plugin/node/plugin-container-module'; +import { FooExt } from '../common/foo-api-rpc'; +import { FooExtImpl } from './foo-ext-impl'; + +import * as fooBarAPI from '@bar/foo'; + +type FooBarApi = typeof fooBarAPI; +type Foo = FooBarApi['Foo']; + +const FooBarApiFactory = Symbol('FooBarApiFactory'); + +// 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 the implementations of our Ext API interfaces (here just one) + bind(FooExt).to(FooExtImpl).inSingletonScope(); + + // Bind our API factory to the module name by which plugins will import it + bindApiFactory('@bar/foo', FooBarApiFactory, FooBarApiFactoryImpl); +}); +``` + +## Implement your API object + +We create a dedicated API object for each individual plugin as part of the module loading process. +Each API object is returned as part of the module loading process if a script imports `@bar/foo` and should therefore match the API definition that we provided in the `*.d.ts` file. +Multiple imports will not lead to the creation of multiple API objects as the `PluginContainerModule` automatically caches the API implementation for us. + +Example `node/foo-init.ts` (continued): + +```typescript +// Creates the @foo/bar API object +@injectable() +class FooBarApiFactoryImpl { + @inject(RPCProtocol) protected readonly rpc: RPCProtocol; + @inject(FooExt) protected readonly fooExt: FooExt; + + @postConstruct() + initialize(): void { + this.rpc.set(FOO_MAIN_RPC_CONTEXT.FOO_EXT, this.fooExt); + } + + // The plugin host expects our API factory to export a `createApi()` method + createApi(plugin: Plugin): FooBarApi { + const self = this; + return { + fooBar: { + getFoo(): Promise { + return self.fooExt.getFooImpl(); + } + } + }; + }; +} +``` + +In the example above the API object creates a local object that will fulfill the API contract. +The implementation details are hidden by the object and it could be a local implementation that only lives inside the plugin host but it could also be an implementation that uses the `RPCProtocol` to communicate with the main application to trigger changes, register functionality or retrieve information. + +### Implement Main-Ext communication + +In this document, we will only highlight the individual parts needed to establish the communication between the main application and the external plugin host. +For a more elaborate example of an API that communicates with the main application, please have a look at the definition of the [Theia Plugin API](https://github.com/eclipse-theia/theia/blob/master/doc/Plugin-API.md). + +First, we need to establish the communication on the RPC protocol by providing an implementation for our own side and generating a proxy for the opposite side. +Proxies are identified using dedicated identifiers so we set them up first, together with the expected interfaces. +`Ext` and `Main` interfaces contain the functions called over RCP and must start with `$`. +Due to the asynchronous nature of the communication over RPC, the result should always be a `Promise` or `PromiseLike`. + +Example `common/foo-api-rpc.ts`: + +```typescript +export const FooMain = Symbol('FooMain'); +export interface FooMain { + $getFooImpl(): Promise; +} + +export const FooExt = Symbol('FooExt'); +export interface FooExt { + // placeholder for callbacks for the main application to the extension +} + +// Plugin host will obtain a proxy using these IDs, main application will register an implementation for it. +export const FOO_PLUGIN_RPC_CONTEXT = { + FOO_MAIN: createProxyIdentifier('FooMain') +}; + +// Main application will obtain a proxy using these IDs, plugin host will register an implementation for it. +export const FOO_MAIN_RPC_CONTEXT = { + FOO_EXT: createProxyIdentifier('FooExt') +}; +``` + +On the plugin host side we can register our implementation and retrieve the proxy as part of our `createAPIFactory` implementation: + +Example `plugin/foo-ext.ts`: + +```typescript +import { inject, injectable } from '@theia/core/shared/inversify'; +import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol'; +import { FooExt, FooMain, FOO_PLUGIN_RPC_CONTEXT } from '../common/foo-api-rpc'; + +@injectable() +export class FooExtImpl implements FooExt { + // Main application RCP counterpart + private proxy: FooMain; + + constructor(@inject(RPCProtocol) rpc: RPCProtocol) { + // Retrieve a proxy for the main side + this.proxy = rpc.getProxy(FOO_PLUGIN_RPC_CONTEXT.FOO_MAIN); + } + + getFooImpl(): Promise { + return this.proxy.$getFooImpl(); + } +} +``` + +On the main side we need to implement the counterpart of the ExtPluginApiProvider, the `MainPluginApiProvider`, and expose it in a browser frontend module. + +> [!NOTE] +> If the same API is also published to headless plugins, then the Main side is actually in the Node backend, not the browser frontend, so the implementation might +then be in the `common/` tree and registered in both the frontend and backend container modules. +> Alternatively, if the API is _only_ published to headless plugins, then it can be implemented in the `node/` tree and can take advantage of capabilities only available in the Node backend. + +Example `main/browser/foo-main.ts`: + +```typescript +@injectable() +export class FooMainImpl implements FooMain { + @inject(MessageService) protected messageService: MessageService; + protected proxy: FooExt; + + constructor(@inject(RPCProtocol) rpc: RPCProtocol) { + // We would use this if we had a need to call back into the plugin-host/plugin + this.proxy = rpc.getProxy(FOO_MAIN_RPC_CONTEXT.FOO_EXT); + } + + async $getFooImpl(): Promise { + this.messageService.info('We were called from the plugin-host at the behest of the plugin.'); + return new Foo(); + } +} + +@injectable() +export class FooMainPluginApiProvider implements MainPluginApiProvider { + @inject(MessageService) protected messageService: MessageService; + @inject(FooMain) protected fooMain: FooMain; + + initialize(rpc: RPCProtocol): void { + this.messageService.info('Initialize RPC communication for FooMain!'); + rpc.set(FOO_PLUGIN_RPC_CONTEXT.FOO_MAIN, this.fooMain); + } +} + +export default new ContainerModule(bind => { + bind(MainPluginApiProvider).to(FooMainPluginApiProvider).inSingletonScope(); + bind(FooMain).to(FooMainImpl).inSingletonScope(); +}); +``` + +In this example, we can already see the big advantage of going to the main application side as we have full access to our Theia services. + +## Usage in a plugin + +When using the API in a plugin the user can simply use the API as follows: + +```typescript +import * as foo from '@bar/foo'; + +foo.fooBar.getFoo(); +``` + +## Adding custom plugin activation events + +When creating a custom plugin API there may also arise a need to trigger the activation of your plugins at a certain point in time. +The events that trigger the activation of a plugin are simply called `activation events`. +By default Theia supports a set of built-in activation events that contains the [activation events from VS Code](https://code.visualstudio.com/api/references/activation-events) as well as some additional Theia-specific events. +Technically, an activation event is nothing more than a unique string fired at a specific point in time. +To add more flexibility to activations events, Theia allows you to provide additional custom activation events when initializing a plugin host. +These additional events can be specified by adopters through the `ADDITIONAL_ACTIVATION_EVENTS` environment variable. +To fire an activation event, you need to call the plugin hosts `$activateByEvent(eventName)` method. + +## Packaging + +When bundling our application with the generated `gen-webpack.node.config.js` we need to make sure that our initialization function is bundled as a `commonjs2` library so it can be dynamically loaded. +Adjust the `webpack.config.js` accordingly: + +```typescript +const configs = require('./gen-webpack.config.js'); +const nodeConfig = require('./gen-webpack.node.config.js'); + +if (nodeConfig.config.entry) { + /** + * Add our initialization function. If unsure, look at the already generated entries for + * the nodeConfig where an entry is added for the default 'backend-init-theia' initialization. + */ + nodeConfig.config.entry['foo-init'] = { + import: require.resolve('@namespace/package/lib/node/foo-init'), + library: { type: 'commonjs2' } + }; +} + +module.exports = [...configs, nodeConfig.config]; + +``` diff --git a/packages/plugin-ext/doc/how-to-add-new-plugin-namespace.md b/packages/plugin-ext/doc/how-to-add-new-plugin-namespace.md deleted file mode 100644 index e8f8611374770..0000000000000 --- a/packages/plugin-ext/doc/how-to-add-new-plugin-namespace.md +++ /dev/null @@ -1,112 +0,0 @@ -# This document describes how to add new plugin api namespace - -New Plugin API namespace should be packaged as Theia extension - -## Provide your API or namespace - -This API developed in the way that you provide your API as separate npm package. -In that package you can declare your api. -Example `foo.d.ts`: - -```typescript - declare module '@bar/foo' { - export namespace fooBar { - export function getFoo(): Foo; - } - } -``` - -## Declare `ExtPluginApiProvider` implementation - -```typescript -@injectable() -export class FooPluginApiProvider implements ExtPluginApiProvider { - provideApi(): ExtPluginApi { - return { - frontendExtApi: { - initPath: '/path/to/foo/api/implementation.js', - initFunction: 'fooInitializationFunction', - initVariable: 'foo_global_variable' - }, - backendInitPath: path.join(__dirname, 'path/to/backend/foo/implementation.js') - }; - } -} -``` - -## Then you need to register `FooPluginApiProvider`, add next sample in your backend module - -Example: - -```typescript - bind(FooPluginApiProvider).toSelf().inSingletonScope(); - bind(Symbol.for(ExtPluginApiProvider)).toService(FooPluginApiProvider); -``` - -## Next you need to implement `ExtPluginApiBackendInitializationFn`, which should handle `@bar/foo` module loading and instantiate `@foo/bar` API object, `path/to/backend/foo/implementation.js` example : - -```typescript -export const provideApi: ExtPluginApiBackendInitializationFn = (rpc: RPCProtocol, pluginManager: PluginManager) => { - cheApiFactory = createAPIFactory(rpc); - plugins = pluginManager; - - if (!isLoadOverride) { - overrideInternalLoad(); - isLoadOverride = true; - } - -}; - -function overrideInternalLoad(): void { - const module = require('module'); - const internalLoad = module._load; - - module._load = function (request: string, parent: any, isMain: {}) { - if (request !== '@bar/foo') { - return internalLoad.apply(this, arguments); - } - - const plugin = findPlugin(parent.filename); - if (plugin) { - let apiImpl = pluginsApiImpl.get(plugin.model.id); - if (!apiImpl) { - apiImpl = cheApiFactory(plugin); - pluginsApiImpl.set(plugin.model.id, apiImpl); - } - return apiImpl; - } - - if (!defaultApi) { - console.warn(`Could not identify plugin for '@bar/foo' require call from ${parent.filename}`); - defaultApi = cheApiFactory(emptyPlugin); - } - - return defaultApi; - }; -} - -function findPlugin(filePath: string): Plugin | undefined { - return plugins.getAllPlugins().find(plugin => filePath.startsWith(plugin.pluginFolder)); -} -``` - -## Next you need to implement `createAPIFactory` factory function - -Example: - -```typescript -import * as fooApi from '@bar/foo'; -export function createAPIFactory(rpc: RPCProtocol): ApiFactory { - const fooBarImpl = new FooBarImpl(rpc); - return function (plugin: Plugin): typeof fooApi { - const FooBar: typeof fooApi.fooBar = { - getFoo(): fooApi.Foo{ - return fooBarImpl.getFooImpl(); - } - } - return { - fooBar : FooBar - }; - } - -``` diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index 8ed3e78710ff1..3143dc53e15be 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -1,37 +1,37 @@ { "name": "@theia/plugin-ext", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Plugin Extension", "main": "lib/common/index.js", "typings": "lib/common/index.d.ts", "dependencies": { - "@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/markers": "1.44.0", - "@theia/messages": "1.44.0", - "@theia/monaco": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/navigator": "1.44.0", - "@theia/notebook": "1.44.0", - "@theia/output": "1.44.0", - "@theia/plugin": "1.44.0", - "@theia/preferences": "1.44.0", - "@theia/scm": "1.44.0", - "@theia/search-in-workspace": "1.44.0", - "@theia/task": "1.44.0", - "@theia/terminal": "1.44.0", - "@theia/timeline": "1.44.0", - "@theia/typehierarchy": "1.44.0", - "@theia/variable-resolver": "1.44.0", - "@theia/workspace": "1.44.0", - "@theia/test": "1.44.0", + "@theia/bulk-edit": "1.54.0", + "@theia/callhierarchy": "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/markers": "1.54.0", + "@theia/messages": "1.54.0", + "@theia/monaco": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", + "@theia/navigator": "1.54.0", + "@theia/notebook": "1.54.0", + "@theia/output": "1.54.0", + "@theia/plugin": "1.54.0", + "@theia/preferences": "1.54.0", + "@theia/scm": "1.54.0", + "@theia/search-in-workspace": "1.54.0", + "@theia/task": "1.54.0", + "@theia/terminal": "1.54.0", + "@theia/test": "1.54.0", + "@theia/timeline": "1.54.0", + "@theia/typehierarchy": "1.54.0", + "@theia/variable-resolver": "1.54.0", + "@theia/workspace": "1.54.0", "@types/mime": "^2.0.1", "@vscode/debugprotocol": "^1.51.0", "@vscode/proxy-agent": "^0.13.2", @@ -46,7 +46,7 @@ "mime": "^2.4.4", "ps-tree": "^1.2.0", "semver": "^7.5.4", - "uuid": "^8.0.0", + "tslib": "^2.6.2", "vhost": "^3.0.2", "vscode-textmate": "^9.0.0" }, @@ -88,7 +88,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0", + "@theia/ext-scripts": "1.54.0", "@types/decompress": "^4.2.2", "@types/escape-html": "^0.0.20", "@types/lodash.clonedeep": "^4.5.3", diff --git a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index 2c3ed04df81b9..de2450bff21bb 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -18,7 +18,7 @@ import * as theia from '@theia/plugin'; import type * as monaco from '@theia/monaco-editor-core'; import { MarkdownString as MarkdownStringDTO } from '@theia/core/lib/common/markdown-rendering'; import { UriComponents } from './uri-components'; -import { CompletionItemTag, SnippetString } from '../plugin/types-impl'; +import { CompletionItemTag, DocumentPasteEditKind, SnippetString } from '../plugin/types-impl'; import { Event as TheiaEvent } from '@theia/core/lib/common/event'; import { URI } from '@theia/core/shared/vscode-uri'; import { SerializedRegExp } from './plugin-api-rpc'; @@ -72,6 +72,17 @@ export interface Range { readonly endColumn: number; } +export interface Position { + /** + * line number (starts at 1) + */ + readonly lineNumber: number, + /** + * column (starts at 1) + */ + readonly column: number +} + export { MarkdownStringDTO as MarkdownString }; export interface SerializedDocumentFilter { @@ -330,17 +341,17 @@ export interface DocumentDropEdit { } export interface DocumentDropEditProviderMetadata { - readonly id: string; + readonly providedDropEditKinds?: readonly DocumentPasteEditKind[]; readonly dropMimeTypes: readonly string[]; } export interface DataTransferFileDTO { + readonly id: string; readonly name: string; readonly uri?: UriComponents; } export interface DataTransferItemDTO { - readonly id: string; readonly asString: string; readonly fileData: DataTransferFileDTO | undefined; readonly uriListData?: ReadonlyArray; @@ -895,3 +906,13 @@ export interface InlineCompletionsProvider; @@ -231,9 +249,11 @@ export interface PluginManagerInitializeParams { globalState: KeysToKeysToAnyValue workspaceState: KeysToKeysToAnyValue env: EnvInit + pluginKind: ExtensionKind extApi?: ExtPluginApi[] webview: WebviewInitData jsonValidation: PluginJsonValidationContribution[] + supportedActivationEvents?: string[] } export interface PluginManagerStartParams { @@ -242,10 +262,9 @@ export interface PluginManagerStartParams { activationEvents: string[] } -export interface PluginManagerExt { - +export interface AbstractPluginManagerExt

    > { /** initialize the manager, should be called only once */ - $init(params: PluginManagerInitializeParams): Promise; + $init(params: P): Promise; /** load and activate plugins */ $start(params: PluginManagerStartParams): Promise; @@ -263,6 +282,8 @@ export interface PluginManagerExt { $activatePlugin(id: string): Promise; } +export interface PluginManagerExt extends AbstractPluginManagerExt { } + export interface CommandRegistryMain { $registerCommand(command: theia.CommandDescription): void; $unregisterCommand(id: string): void; @@ -297,6 +318,7 @@ export interface TerminalServiceExt { $handleTerminalLink(link: ProvidedTerminalLink): Promise; getEnvironmentVariableCollection(extensionIdentifier: string): theia.GlobalEnvironmentVariableCollection; $setShell(shell: string): void; + $reportOutputMatch(observerId: string, groups: string[]): void; } export interface OutputChannelRegistryExt { createOutputChannel(name: string, pluginInfo: PluginInfo): theia.OutputChannel, @@ -320,15 +342,15 @@ export interface TerminalServiceMain { * Create new Terminal with Terminal options. * @param options - object with parameters to create new terminal. */ - $createTerminal(id: string, options: theia.TerminalOptions, parentId?: string, isPseudoTerminal?: boolean): Promise; + $createTerminal(id: string, options: TerminalOptions, parentId?: string, isPseudoTerminal?: boolean): Promise; /** * Send text to the terminal by id. * @param id - terminal widget id. * @param text - text content. - * @param addNewLine - in case true - add new line after the text, otherwise - don't apply new line. + * @param shouldExecute - in case true - Indicates that the text being sent should be executed rather than just inserted in the terminal. */ - $sendText(id: string, text: string, addNewLine?: boolean): void; + $sendText(id: string, text: string, shouldExecute?: boolean): void; /** * Write data to the terminal by id. @@ -434,6 +456,24 @@ export interface TerminalServiceMain { * @param providerId id of the terminal link provider to be unregistered. */ $unregisterTerminalLinkProvider(providerId: string): Promise; + + /** + * Register a new terminal observer. + * @param providerId id of the terminal link provider to be registered. + * @param nrOfLinesToMatch the number of lines to match the outputMatcherRegex against + * @param outputMatcherRegex the regex to match the output to + */ + $registerTerminalObserver(id: string, nrOfLinesToMatch: number, outputMatcherRegex: string): unknown; + + /** + * Unregister the terminal observer with the specified id. + * @param providerId id of the terminal observer to be unregistered. + */ + $unregisterTerminalObserver(id: string): unknown; +} + +export interface TerminalOptions extends theia.TerminalOptions { + iconUrl?: string | { light: string; dark: string } | ThemeIcon; } export interface AutoFocus { @@ -478,11 +518,9 @@ export interface StatusBarMessageRegistryMain { $dispose(id: string): void; } -export type Item = string | theia.QuickPickItem; - export interface QuickOpenExt { $onItemSelected(handle: number): void; - $validateInput(input: string): Promise | undefined; + $validateInput(input: string): Promise; $acceptOnDidAccept(sessionId: number): Promise; $acceptDidChangeValue(sessionId: number, changedValue: string): Promise; @@ -493,11 +531,10 @@ export interface QuickOpenExt { $onDidChangeSelection(sessionId: number, handles: number[]): void; /* eslint-disable max-len */ - showQuickPick(itemsOrItemsPromise: Array | Promise>, options: theia.QuickPickOptions & { canPickMany: true; }, + showQuickPick(plugin: Plugin, itemsOrItemsPromise: Array | Promise>, options: theia.QuickPickOptions & { canPickMany: true; }, token?: theia.CancellationToken): Promise | undefined>; - showQuickPick(itemsOrItemsPromise: string[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: Array | Promise>, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: Item[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: string[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: Array | Promise>, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; showInput(options?: theia.InputBoxOptions, token?: theia.CancellationToken): PromiseLike; // showWorkspaceFolderPick(options?: theia.WorkspaceFolderPickOptions, token?: theia.CancellationToken): Promise @@ -617,22 +654,36 @@ export interface WorkspaceFolderPickOptionsMain { ignoreFocusOut?: boolean; } -export type TransferQuickPickItems = TransferQuickPickItemValue | TransferQuickPickSeparator; - -export interface TransferQuickPickItemValue extends theia.QuickPickItem { +export interface TransferQuickPickItem { handle: number; - type?: 'item' + kind: 'item' | 'separator', + label: string; + iconUrl?: string | { light: string; dark: string } | ThemeIcon; + description?: string; + detail?: string; + picked?: boolean; + alwaysShow?: boolean; + buttons?: readonly TransferQuickInputButton[]; } -export interface TransferQuickPickSeparator extends theia.QuickPickItem { - handle: number; - type: 'separator'; +export interface TransferQuickPickOptions { + title?: string; + placeHolder?: string; + matchOnDescription?: boolean; + matchOnDetail?: boolean; + matchOnLabel?: boolean; + autoFocusOnList?: boolean; + ignoreFocusLost?: boolean; + canPickMany?: boolean; + contextKey?: string; + activeItem?: Promise | T; + onDidFocus?: (entry: T) => void; } -export interface TransferQuickInputButton extends theia.QuickInputButton { - iconPath: theia.Uri | { light: theia.Uri, dark: theia.Uri } | ThemeIcon; - iconClass?: string; +export interface TransferQuickInputButton { handle?: number; + readonly iconUrl?: string | { light: string; dark: string } | ThemeIcon; + readonly tooltip?: string | undefined; } export type TransferQuickInput = TransferQuickPick | TransferInputBox; @@ -651,7 +702,7 @@ export interface TransferQuickPick extends BaseTransferQuickInput { value?: string; placeholder?: string; buttons?: TransferQuickInputButton[]; - items?: TransferQuickPickItems[]; + items?: TransferQuickPickItem[]; activeItems?: ReadonlyArray; selectedItems?: ReadonlyArray; canSelectMany?: boolean; @@ -681,8 +732,8 @@ export interface IInputBoxOptions { } export interface QuickOpenMain { - $show(instance: number, options: PickOptions, token: CancellationToken): Promise; - $setItems(instance: number, items: TransferQuickPickItems[]): Promise; + $show(instance: number, options: TransferQuickPickOptions, token: CancellationToken): Promise; + $setItems(instance: number, items: TransferQuickPickItem[]): Promise; $setError(instance: number, error: Error): Promise; $input(options: theia.InputBoxOptions, validateInput: boolean, token: CancellationToken): Promise; $createOrUpdate(params: TransferQuickInput): Promise; @@ -754,9 +805,9 @@ export interface RegisterTreeDataProviderOptions { } export interface TreeViewRevealOptions { - select: boolean - focus: boolean - expand: boolean | number + readonly select: boolean + readonly focus: boolean + readonly expand: boolean | number } export interface TreeViewsMain { @@ -863,7 +914,8 @@ export interface WindowMain { } export interface WindowStateExt { - $onWindowStateChanged(focus: boolean): void; + $onDidChangeWindowFocus(focused: boolean): void; + $onDidChangeWindowActive(active: boolean): void; } export interface NotificationExt { @@ -1178,7 +1230,7 @@ export interface UndoStopOptions { } export interface ApplyEditsOptions extends UndoStopOptions { - setEndOfLine: EndOfLine; + setEndOfLine: EndOfLine | undefined; } export interface ThemeColor { @@ -1286,6 +1338,8 @@ export interface TextEditorsMain { $tryApplyEdits(id: string, modelVersionId: number, edits: SingleEditOperation[], opts: ApplyEditsOptions): Promise; $tryApplyWorkspaceEdit(workspaceEditDto: WorkspaceEditDto, metadata?: WorkspaceEditMetadataDto): Promise; $tryInsertSnippet(id: string, template: string, selections: Range[], opts: UndoStopOptions): Promise; + $save(uri: UriComponents): PromiseLike; + $saveAs(uri: UriComponents): PromiseLike; $saveAll(includeUntitled?: boolean): Promise; // $getDiffInformation(id: string): Promise; } @@ -1355,7 +1409,6 @@ export interface DocumentsMain { $tryShowDocument(uri: UriComponents, options?: TextDocumentShowOptions): Promise; $tryOpenDocument(uri: UriComponents): Promise; $trySaveDocument(uri: UriComponents): Promise; - $tryCloseDocument(uri: UriComponents): Promise; } export interface EnvMain { @@ -1468,12 +1521,10 @@ export interface WorkspaceEditEntryMetadataDto { needsConfirmation: boolean; label: string; description?: string; - iconPath?: { - id: string; - } | { + iconPath?: ThemeIcon | { light: UriComponents; dark: UriComponents; - } | ThemeIcon; + }; } export interface WorkspaceFileEditDto { @@ -1531,6 +1582,16 @@ export interface WorkspaceNotebookCellEditDto { cellEdit: CellEditOperationDto; } +export namespace WorkspaceNotebookCellEditDto { + export function is(arg: WorkspaceNotebookCellEditDto | WorkspaceFileEditDto | WorkspaceTextEditDto): arg is WorkspaceNotebookCellEditDto { + return !!arg + && 'resource' in arg + && 'cellEdit' in arg + && arg.cellEdit !== null + && typeof arg.cellEdit === 'object'; + } +} + export interface WorkspaceEditDto { edits: Array; } @@ -1817,12 +1878,12 @@ export interface WebviewViewsMain extends Disposable { } export interface CustomEditorsExt { - $resolveWebviewEditor( + $resolveWebviewEditor( resource: UriComponents, newWebviewHandle: string, viewType: string, title: string, - widgetOpenerOptions: T | undefined, + position: number, options: theia.WebviewPanelOptions, cancellation: CancellationToken): Promise; $createCustomDocument(resource: UriComponents, viewType: string, openContext: theia.CustomDocumentOpenContext, cancellation: CancellationToken): Promise<{ editable: boolean }>; @@ -1845,7 +1906,6 @@ export interface CustomEditorsMain { $registerTextEditorProvider(viewType: string, options: theia.WebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void; $registerCustomEditorProvider(viewType: string, options: theia.WebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void; $unregisterEditorProvider(viewType: string): void; - $createCustomEditorPanel(handle: string, title: string, widgetOpenerOptions: T | undefined, options: theia.WebviewPanelOptions & theia.WebviewOptions): Promise; $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; $onContentChange(resource: UriComponents, viewType: string): void; } @@ -1923,6 +1983,8 @@ export interface DebugExt { debugConfiguration: DebugConfiguration ): Promise; + $onDidChangeActiveFrame(frame: DebugStackFrameDTO | undefined): void; + $onDidChangeActiveThread(thread: DebugThreadDTO | undefined): void; $createDebugSession(debugConfiguration: DebugConfiguration, workspaceFolder: string | undefined): Promise; $terminateDebugSession(sessionId: string): Promise; $getTerminalCreationOptions(debugType: string): Promise; @@ -1967,7 +2029,7 @@ export interface IFileChangeDto { } export interface FileSystemMain { - $registerFileSystemProvider(handle: number, scheme: string, capabilities: files.FileSystemProviderCapabilities): void; + $registerFileSystemProvider(handle: number, scheme: string, capabilities: files.FileSystemProviderCapabilities, readonlyMessage?: MarkdownString): void; $unregisterProvider(handle: number): void; $onFileSystemChange(handle: number, resource: IFileChangeDto[]): void; @@ -2169,6 +2231,9 @@ export interface TestingExt { /** Configures a test run config. */ $onConfigureRunProfile(controllerId: string, profileId: string): void; + /** Sets the default on a given run profile */ + $onDidChangeDefault(controllerId: string, profileId: string, isDefault: boolean): void; + $onRunControllerTests(reqs: TestRunRequestDTO[]): void; /** Asks the controller to refresh its tests */ @@ -2177,6 +2242,17 @@ export interface TestingExt { $onResolveChildren(controllerId: string, path: string[]): void; } +// based from https://github.com/microsoft/vscode/blob/1.85.1/src/vs/workbench/api/common/extHostUrls.ts +export interface UriExt { + registerUriHandler(handler: theia.UriHandler, plugin: PluginInfo): theia.Disposable; + $handleExternalUri(uri: UriComponents): Promise; +} + +export interface UriMain { + $registerUriHandler(extensionId: string, extensionName: string): void; + $unregisterUriHandler(extensionId: string): void; +} + export interface TestControllerUpdate { label: string; canRefresh: boolean; @@ -2206,7 +2282,7 @@ export interface TestingMain { // Test runs - $notifyTestRunCreated(controllerId: string, run: TestRunDTO): void; + $notifyTestRunCreated(controllerId: string, run: TestRunDTO, preserveFocus: boolean): void; $notifyTestStateChanged(controllerId: string, runId: string, stateChanges: TestStateChangeDTO[], outputChanges: TestOutputDTO[]): void; $notifyTestRunEnded(controllerId: string, runId: string): void; } @@ -2254,7 +2330,8 @@ export const PLUGIN_RPC_CONTEXT = { TABS_MAIN: >createProxyIdentifier('TabsMain'), TELEMETRY_MAIN: >createProxyIdentifier('TelemetryMain'), LOCALIZATION_MAIN: >createProxyIdentifier('LocalizationMain'), - TESTING_MAIN: createProxyIdentifier('TestingMain') + TESTING_MAIN: createProxyIdentifier('TestingMain'), + URI_MAIN: createProxyIdentifier('UriMain') }; export const MAIN_RPC_CONTEXT = { @@ -2271,7 +2348,7 @@ export const MAIN_RPC_CONTEXT = { NOTEBOOKS_EXT: createProxyIdentifier('NotebooksExt'), NOTEBOOK_DOCUMENTS_EXT: createProxyIdentifier('NotebookDocumentsExt'), NOTEBOOK_EDITORS_EXT: createProxyIdentifier('NotebookEditorsExt'), - NOTEBOOK_RENDERERS_EXT: createProxyIdentifier('NotebooksExt'), + NOTEBOOK_RENDERERS_EXT: createProxyIdentifier('NotebooksRenderersExt'), NOTEBOOK_KERNELS_EXT: createProxyIdentifier('NotebookKernelsExt'), TERMINAL_EXT: createProxyIdentifier('TerminalServiceExt'), OUTPUT_CHANNEL_REGISTRY_EXT: createProxyIdentifier('OutputChannelRegistryExt'), @@ -2296,7 +2373,8 @@ export const MAIN_RPC_CONTEXT = { COMMENTS_EXT: createProxyIdentifier('CommentsExt'), TABS_EXT: createProxyIdentifier('TabsExt'), TELEMETRY_EXT: createProxyIdentifier('TelemetryExt)'), - TESTING_EXT: createProxyIdentifier('TestingExt') + TESTING_EXT: createProxyIdentifier('TestingExt'), + URI_EXT: createProxyIdentifier('UriExt') }; export interface TasksExt { @@ -2320,13 +2398,14 @@ export interface TasksMain { } export interface AuthenticationExt { - $getSessions(id: string, scopes?: string[]): Promise>; - $createSession(id: string, scopes: string[]): Promise; + $getSessions(providerId: string, scopes: string[] | undefined, options: theia.AuthenticationProviderSessionOptions): Promise>; + $createSession(id: string, scopes: string[], options: theia.AuthenticationProviderSessionOptions): Promise; $removeSession(id: string, sessionId: string): Promise; $onDidChangeAuthenticationSessions(provider: theia.AuthenticationProviderInformation): Promise; } export interface AuthenticationMain { + $getAccounts(providerId: string): Thenable; $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): void; $unregisterAuthenticationProvider(id: string): void; $onDidChangeSessions(providerId: string, event: AuthenticationProviderAuthenticationSessionsChangeEvent): void; @@ -2342,7 +2421,7 @@ export interface NotebookOutputItemDto { export interface NotebookOutputDto { outputId: string; items: NotebookOutputItemDto[]; - metadata?: Record; + metadata?: Record; } export interface NotebookCellDataDto { @@ -2429,6 +2508,10 @@ export type NotebookRawContentEventDto = readonly outputItems: NotebookOutputItemDto[]; readonly append: boolean; } + | { + readonly kind: notebookCommon.NotebookCellsChangeType.ChangeDocumentMetadata + readonly metadata: notebookCommon.NotebookDocumentMetadata; + } | notebookCommon.NotebookCellsChangeLanguageEvent | notebookCommon.NotebookCellsChangeMetadataEvent | notebookCommon.NotebookCellsChangeInternalMetadataEvent @@ -2471,7 +2554,7 @@ export interface NotebookKernelDto { id: string; notebookType: string; extensionId: string; - // extensionLocation: UriComponents; + extensionLocation: UriComponents; label: string; detail?: string; description?: string; @@ -2590,6 +2673,7 @@ export interface NotebookDocumentsExt { export interface NotebookDocumentsAndEditorsExt { $acceptDocumentsAndEditorsDelta(delta: NotebookDocumentsAndEditorsDelta): Promise; + $acceptActiveCellEditorChange(newActiveEditor: string | null): void; } export interface NotebookDocumentsAndEditorsMain extends Disposable { @@ -2650,6 +2734,7 @@ export interface IdentifiableInlineCompletion extends InlineCompletion { idx: number; } +export const LocalizationExt = Symbol('LocalizationExt'); export interface LocalizationExt { translateMessage(pluginId: string, details: StringDetails): string; getBundle(pluginId: string): Record | undefined; diff --git a/packages/plugin-ext/src/common/plugin-ext-api-contribution.ts b/packages/plugin-ext/src/common/plugin-ext-api-contribution.ts index 41455e26e98b3..522987b7ca89b 100644 --- a/packages/plugin-ext/src/common/plugin-ext-api-contribution.ts +++ b/packages/plugin-ext/src/common/plugin-ext-api-contribution.ts @@ -19,26 +19,53 @@ import { interfaces } from '@theia/core/shared/inversify'; export const ExtPluginApiProvider = 'extPluginApi'; /** - * Provider for extension API description + * Provider for extension API description. */ export interface ExtPluginApiProvider { /** - * Provide API description + * Provide API description. */ provideApi(): ExtPluginApi; } /** - * Plugin API extension description. - * This interface describes scripts for both plugin runtimes: frontend(WebWorker) and backend(NodeJs) + * Provider for backend extension API description. */ -export interface ExtPluginApi { +export interface ExtPluginBackendApiProvider { + /** + * Provide API description. + */ + provideApi(): ExtPluginBackendApi; +} + +/** + * Provider for frontend extension API description. + */ +export interface ExtPluginFrontendApiProvider { + /** + * Provide API description. + */ + provideApi(): ExtPluginFrontendApi; +} + +/** + * Backend Plugin API extension description. + * This interface describes a script for the backend(NodeJs) runtime. + */ +export interface ExtPluginBackendApi { /** * Path to the script which should be loaded to provide api, module should export `provideApi` function with * [ExtPluginApiBackendInitializationFn](#ExtPluginApiBackendInitializationFn) signature */ backendInitPath?: string; +} + +/** + * Frontend Plugin API extension description. + * This interface describes a script for the frontend(WebWorker) runtime. + */ +export interface ExtPluginFrontendApi { /** * Initialization information for frontend part of Plugin API @@ -46,6 +73,12 @@ export interface ExtPluginApi { frontendExtApi?: FrontendExtPluginApi; } +/** + * Plugin API extension description. + * This interface describes scripts for both plugin runtimes: frontend(WebWorker) and backend(NodeJs) + */ +export interface ExtPluginApi extends ExtPluginBackendApi, ExtPluginFrontendApi { } + export interface ExtPluginApiFrontendInitializationFn { (rpc: RPCProtocol, plugins: Map): void; } diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 19f6e0d7920f4..222b699797831 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -31,7 +31,7 @@ export { PluginIdentifiers }; export const hostedServicePath = '/services/hostedPlugin'; /** - * Plugin engine (API) type, i.e. 'theiaPlugin', 'vscode', etc. + * Plugin engine (API) type, i.e. 'theiaPlugin', 'vscode', 'theiaHeadlessPlugin', etc. */ export type PluginEngine = string; @@ -49,6 +49,8 @@ export interface PluginPackage { theiaPlugin?: { frontend?: string; backend?: string; + /* Requires the `@theia/plugin-ext-headless` extension. */ + headless?: string; }; main?: string; browser?: string; @@ -101,6 +103,7 @@ export interface PluginPackageContribution { terminal?: PluginPackageTerminal; notebooks?: PluginPackageNotebook[]; notebookRenderer?: PluginNotebookRendererContribution[]; + notebookPreload?: PluginPackageNotebookPreload[]; } export interface PluginPackageNotebook { @@ -118,6 +121,11 @@ export interface PluginNotebookRendererContribution { readonly requiresMessaging?: 'always' | 'optional' | 'never' } +export interface PluginPackageNotebookPreload { + type: string; + entrypoint: string; +} + export interface PluginPackageAuthenticationProvider { id: string; label: string; @@ -190,6 +198,7 @@ export interface PluginPackageViewWelcome { export interface PluginPackageCommand { command: string; title: string; + shortTitle?: string; original?: string; category?: string; icon?: string | { light: string; dark: string; }; @@ -445,7 +454,9 @@ export enum PluginDeployerEntryType { FRONTEND, - BACKEND + BACKEND, + + HEADLESS // Deployed in the Theia Node server outside the context of a frontend/backend connection } /** @@ -571,6 +582,7 @@ export interface PluginModel { export interface PluginEntryPoint { frontend?: string; backend?: string; + headless?: string; } /** @@ -605,8 +617,8 @@ export interface PluginContribution { terminalProfiles?: TerminalProfile[]; notebooks?: NotebookContribution[]; notebookRenderer?: NotebookRendererContribution[]; + notebookPreload?: notebookPreloadContribution[]; } - export interface NotebookContribution { type: string; displayName: string; @@ -622,6 +634,11 @@ export interface NotebookRendererContribution { readonly requiresMessaging?: 'always' | 'optional' | 'never' } +export interface notebookPreloadContribution { + type: string; + entrypoint: string; +} + export interface AuthenticationProviderInformation { id: string; label: string; @@ -842,6 +859,7 @@ export interface ViewWelcome { export interface PluginCommand { command: string; title: string; + shortTitle?: string; originalTitle?: string; category?: string; iconUrl?: IconUrl; @@ -966,6 +984,7 @@ export interface PluginDeployerHandler { deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise; deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise; + getDeployedPlugins(): Promise; getDeployedPluginsById(pluginId: string): DeployedPlugin[]; getDeployedPlugin(pluginId: PluginIdentifiers.VersionedId): DeployedPlugin | undefined; diff --git a/packages/plugin-ext/src/common/proxy-handler.ts b/packages/plugin-ext/src/common/proxy-handler.ts deleted file mode 100644 index 75beca868c6ec..0000000000000 --- a/packages/plugin-ext/src/common/proxy-handler.ts +++ /dev/null @@ -1,126 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2022 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 - ********************************************************************************/ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Channel, RpcProtocol, RpcProtocolOptions } from '@theia/core/'; -import { RpcMessageDecoder, RpcMessageEncoder } from '@theia/core/lib/common/message-rpc/rpc-message-encoder'; -import { Deferred } from '@theia/core/lib/common/promise-util'; - -export interface RpcHandlerOptions { - id: string - encoder: RpcMessageEncoder, - decoder: RpcMessageDecoder -} -export interface ProxyHandlerOptions extends RpcHandlerOptions { - channelProvider: () => Promise, -} - -export interface InvocationHandlerOptions extends RpcHandlerOptions { - target: any -} -/** - * A proxy handler that will send any method invocation on the proxied object - * as a rcp protocol message over a channel. - */ -export class ClientProxyHandler implements ProxyHandler { - private rpcDeferred: Deferred = new Deferred(); - private isRpcInitialized = false; - - readonly id: string; - private readonly channelProvider: () => Promise; - private readonly encoder: RpcMessageEncoder; - private readonly decoder: RpcMessageDecoder; - - constructor(options: ProxyHandlerOptions) { - Object.assign(this, options); - } - - private initializeRpc(): void { - const clientOptions: RpcProtocolOptions = { encoder: this.encoder, decoder: this.decoder, mode: 'clientOnly' }; - this.channelProvider().then(channel => { - const rpc = new RpcProtocol(channel, undefined, clientOptions); - this.rpcDeferred.resolve(rpc); - this.isRpcInitialized = true; - }); - } - - get(target: any, name: string, receiver: any): any { - if (!this.isRpcInitialized) { - this.initializeRpc(); - } - - if (target[name] || name.charCodeAt(0) !== 36 /* CharCode.DollarSign */) { - return target[name]; - } - const isNotify = this.isNotification(name); - return (...args: any[]) => { - const method = name.toString(); - return this.rpcDeferred.promise.then(async (connection: RpcProtocol) => { - if (isNotify) { - connection.sendNotification(method, args); - } else { - return await connection.sendRequest(method, args) as Promise; - } - }); - }; - } - - /** - * Return whether the given property represents a notification. If true, - * the promise returned from the invocation will resolve immediately to `undefined` - * - * A property leads to a notification rather than a method call if its name - * begins with `notify` or `on`. - * - * @param p - The property being called on the proxy. - * @return Whether `p` represents a notification. - */ - protected isNotification(p: PropertyKey): boolean { - let propertyString = p.toString(); - if (propertyString.charCodeAt(0) === 36/* CharCode.DollarSign */) { - propertyString = propertyString.substring(1); - } - return propertyString.startsWith('notify') || propertyString.startsWith('on'); - } -} - -export class RpcInvocationHandler { - readonly id: string; - readonly target: any; - - private rpcDeferred: Deferred = new Deferred(); - private readonly encoder: RpcMessageEncoder; - private readonly decoder: RpcMessageDecoder; - - constructor(options: InvocationHandlerOptions) { - Object.assign(this, options); - } - - listen(channel: Channel): void { - const serverOptions: RpcProtocolOptions = { encoder: this.encoder, decoder: this.decoder, mode: 'serverOnly' }; - const server = new RpcProtocol(channel, (method: string, args: any[]) => this.handleRequest(method, args), serverOptions); - server.onNotification((e: { method: string, args: any }) => this.onNotification(e.method, e.args)); - this.rpcDeferred.resolve(server); - } - - protected handleRequest(method: string, args: any[]): Promise { - return this.rpcDeferred.promise.then(() => this.target[method](...args)); - } - - protected onNotification(method: string, args: any[]): void { - this.target[method](...args); - } -} - diff --git a/packages/plugin-ext/src/common/rpc-protocol.ts b/packages/plugin-ext/src/common/rpc-protocol.ts index 107c639ef2c60..1cc84d49e4648 100644 --- a/packages/plugin-ext/src/common/rpc-protocol.ts +++ b/packages/plugin-ext/src/common/rpc-protocol.ts @@ -22,12 +22,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Channel, Disposable, DisposableCollection, isObject, ReadBuffer, URI, WriteBuffer } from '@theia/core'; +import { Channel, Disposable, DisposableCollection, isObject, ReadBuffer, RpcProtocol, URI, WriteBuffer } from '@theia/core'; import { Emitter, Event } from '@theia/core/lib/common/event'; -import { ChannelMultiplexer, MessageProvider } from '@theia/core/lib/common/message-rpc/channel'; -import { MsgPackMessageDecoder, MsgPackMessageEncoder } from '@theia/core/lib/common/message-rpc/rpc-message-encoder'; +import { MessageProvider } from '@theia/core/lib/common/message-rpc/channel'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer'; -import { ClientProxyHandler, RpcInvocationHandler } from './proxy-handler'; import { MsgPackExtensionManager } from '@theia/core/lib/common/message-rpc/msg-pack-extension-manager'; import { URI as VSCodeURI } from '@theia/core/shared/vscode-uri'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; @@ -38,7 +36,7 @@ export interface MessageConnection { onMessage: Event; } -export const RPCProtocol = Symbol('RPCProtocol'); +export const RPCProtocol = Symbol.for('RPCProtocol'); export interface RPCProtocol extends Disposable { /** * Returns a proxy to an object addressable/named in the plugin process or in the main process. @@ -78,21 +76,38 @@ export namespace ConnectionClosedError { } export class RPCProtocolImpl implements RPCProtocol { - private readonly locals = new Map(); + private readonly locals = new Map(); private readonly proxies = new Map(); - private readonly multiplexer: ChannelMultiplexer; - private readonly encoder = new MsgPackMessageEncoder(); - private readonly decoder = new MsgPackMessageDecoder(); + private readonly rpc: RpcProtocol; private readonly toDispose = new DisposableCollection( Disposable.create(() => { /* mark as no disposed */ }) ); constructor(channel: Channel) { - this.toDispose.push(this.multiplexer = new ChannelMultiplexer(new BatchingChannel(channel))); + this.rpc = new RpcProtocol(new BatchingChannel(channel), (method, args) => this.handleRequest(method, args)); + this.rpc.onNotification((evt: { method: string; args: any[]; }) => this.handleNotification(evt.method, evt.args)); this.toDispose.push(Disposable.create(() => this.proxies.clear())); } + handleNotification(method: any, args: any[]): void { + const serviceId = args[0] as string; + const handler: any = this.locals.get(serviceId); + if (!handler) { + throw new Error(`no local service handler with id ${serviceId}`); + } + handler[method](...(args.slice(1))); + } + + handleRequest(method: string, args: any[]): Promise { + const serviceId = args[0] as string; + const handler: any = this.locals.get(serviceId); + if (!handler) { + throw new Error(`no local service handler with id ${serviceId}`); + } + return handler[method](...(args.slice(1))); + } + dispose(): void { this.toDispose.dispose(); } @@ -114,36 +129,55 @@ export class RPCProtocolImpl implements RPCProtocol { } protected createProxy(proxyId: string): T { - const handler = new ClientProxyHandler({ id: proxyId, encoder: this.encoder, decoder: this.decoder, channelProvider: () => this.multiplexer.open(proxyId) }); + const handler = { + get: (target: any, name: string, receiver: any): any => { + if (target[name] || name.charCodeAt(0) !== 36 /* CharCode.DollarSign */) { + // not a remote property + return target[name]; + } + const isNotify = this.isNotification(name); + return async (...args: any[]) => { + const method = name.toString(); + if (isNotify) { + this.rpc.sendNotification(method, [proxyId, ...args]); + } else { + return await this.rpc.sendRequest(method, [proxyId, ...args]) as Promise; + } + }; + } + + }; return new Proxy(Object.create(null), handler); } + /** + * Return whether the given property represents a notification. If true, + * the promise returned from the invocation will resolve immediately to `undefined` + * + * A property leads to a notification rather than a method call if its name + * begins with `notify` or `on`. + * + * @param p - The property being called on the proxy. + * @return Whether `p` represents a notification. + */ + protected isNotification(p: PropertyKey): boolean { + let propertyString = p.toString(); + if (propertyString.charCodeAt(0) === 36/* CharCode.DollarSign */) { + propertyString = propertyString.substring(1); + } + return propertyString.startsWith('notify') || propertyString.startsWith('on'); + } + set(identifier: ProxyIdentifier, instance: R): R { if (this.isDisposed) { throw ConnectionClosedError.create(); } - const invocationHandler = this.locals.get(identifier.id); - if (!invocationHandler) { - const handler = new RpcInvocationHandler({ id: identifier.id, target: instance, encoder: this.encoder, decoder: this.decoder }); - - const channel = this.multiplexer.getOpenChannel(identifier.id); - if (channel) { - handler.listen(channel); - } else { - const channelOpenListener = this.multiplexer.onDidOpenChannel(event => { - if (event.id === identifier.id) { - handler.listen(event.channel); - channelOpenListener.dispose(); - } - }); - } - - this.locals.set(identifier.id, handler); + if (!this.locals.has(identifier.id)) { + this.locals.set(identifier.id, instance); if (Disposable.is(instance)) { this.toDispose.push(instance); } this.toDispose.push(Disposable.create(() => this.locals.delete(identifier.id))); - } return instance; } diff --git a/packages/plugin-ext/src/common/test-types.ts b/packages/plugin-ext/src/common/test-types.ts index c1d74d23ba8e5..56c2ec4015262 100644 --- a/packages/plugin-ext/src/common/test-types.ts +++ b/packages/plugin-ext/src/common/test-types.ts @@ -27,6 +27,7 @@ import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; import { UriComponents } from './uri-components'; import { Location, Range } from './plugin-api-rpc-model'; import { isObject } from '@theia/core'; +import * as languageProtocol from '@theia/core/shared/vscode-languageserver-protocol'; export enum TestRunProfileKind { Run = 1, @@ -74,16 +75,30 @@ export interface TestFailureDTO extends TestStateChangeDTO { readonly duration?: number; } +export namespace TestFailureDTO { + export function is(ref: unknown): ref is TestFailureDTO { + return isObject(ref) + && (ref.state === TestExecutionState.Failed || ref.state === TestExecutionState.Errored); + } +} export interface TestSuccessDTO extends TestStateChangeDTO { readonly state: TestExecutionState.Passed; readonly duration?: number; } +export interface TestMessageStackFrameDTO { + uri?: languageProtocol.DocumentUri; + position?: languageProtocol.Position; + label: string; +} + export interface TestMessageDTO { readonly expected?: string; readonly actual?: string; - readonly location?: Location; + readonly location?: languageProtocol.Location; readonly message: string | MarkdownString; + readonly contextValue?: string; + readonly stackTrace?: TestMessageStackFrameDTO[]; } export interface TestItemDTO { @@ -106,6 +121,7 @@ export interface TestRunRequestDTO { name: string; includedTests: string[][]; // array of paths excludedTests: string[][]; // array of paths + preserveFocus: boolean; } export interface TestItemReference { @@ -131,3 +147,22 @@ export namespace TestItemReference { } } +export interface TestMessageArg { + testItemReference: TestItemReference | undefined, + testMessage: TestMessageDTO +} + +export namespace TestMessageArg { + export function is(arg: unknown): arg is TestMessageArg { + return isObject(arg) + && isObject(arg.testMessage) + && (MarkdownString.is(arg.testMessage.message) || typeof arg.testMessage.message === 'string'); + } + + export function create(testItemReference: TestItemReference | undefined, testMessageDTO: TestMessageDTO): TestMessageArg { + return { + testItemReference: testItemReference, + testMessage: testMessageDTO + }; + } +} diff --git a/packages/plugin-ext/src/common/uri-components.ts b/packages/plugin-ext/src/common/uri-components.ts index f67a95657f3bd..447cecc930818 100644 --- a/packages/plugin-ext/src/common/uri-components.ts +++ b/packages/plugin-ext/src/common/uri-components.ts @@ -18,7 +18,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import URI, { UriComponents } from '@theia/core/lib/common/uri'; +import { UriComponents } from '@theia/core/lib/common/uri'; export { UriComponents }; @@ -79,7 +79,3 @@ export namespace Schemes { export const webviewPanel = 'webview-panel'; } - -export function theiaUritoUriComponents(uri: URI): UriComponents { - return uri.toComponents(); -} diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 13679f53bd200..fb4c21585fcf5 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -21,31 +21,29 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import debounce = require('@theia/core/shared/lodash.debounce'); -import { UUID } from '@theia/core/shared/@phosphor/coreutils'; -import { injectable, inject, interfaces, named, postConstruct } from '@theia/core/shared/inversify'; +import { generateUuid } from '@theia/core/lib/common/uuid'; +import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { PluginWorker } from './plugin-worker'; -import { PluginMetadata, getPluginId, HostedPluginServer, DeployedPlugin, PluginServer, PluginIdentifiers } from '../../common/plugin-protocol'; +import { getPluginId, DeployedPlugin, HostedPluginServer } from '../../common/plugin-protocol'; import { HostedPluginWatcher } from './hosted-plugin-watcher'; -import { MAIN_RPC_CONTEXT, PluginManagerExt, ConfigStorage, UIKind } from '../../common/plugin-api-rpc'; +import { ExtensionKind, MAIN_RPC_CONTEXT, PluginManagerExt, UIKind } from '../../common/plugin-api-rpc'; import { setUpPluginApi } from '../../main/browser/main-context'; import { RPCProtocol, RPCProtocolImpl } from '../../common/rpc-protocol'; import { - Disposable, DisposableCollection, Emitter, isCancelled, - ILogger, ContributionProvider, CommandRegistry, WillExecuteCommandEvent, - CancellationTokenSource, RpcProxy, ProgressService, nls + Disposable, DisposableCollection, isCancelled, + CommandRegistry, WillExecuteCommandEvent, + CancellationTokenSource, ProgressService, nls, + RpcProxy } from '@theia/core'; import { PreferenceServiceImpl, PreferenceProviderProvider } from '@theia/core/lib/browser/preferences'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { PluginContributionHandler } from '../../main/browser/plugin-contribution-handler'; import { getQueryParameters } from '../../main/browser/env-main'; -import { MainPluginApiProvider } from '../../common/plugin-ext-api-contribution'; -import { PluginPathsService } from '../../main/common/plugin-paths-protocol'; import { getPreferences } from '../../main/browser/preference-registry-main'; -import { Deferred } from '@theia/core/lib/common/promise-util'; +import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util'; import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; -import { WaitUntilEvent } from '@theia/core/lib/common/event'; +import { Event, WaitUntilEvent } from '@theia/core/lib/common/event'; import { FileSearchService } from '@theia/file-search/lib/common/file-search-service'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { PluginViewRegistry } from '../../main/browser/view/plugin-view-registry'; @@ -55,7 +53,6 @@ import { WebviewEnvironment } from '../../main/browser/webview/webview-environme import { WebviewWidget } from '../../main/browser/webview/webview'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import URI from '@theia/core/lib/common/uri'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { environment } from '@theia/core/shared/@theia/application-package/lib/environment'; @@ -66,29 +63,46 @@ import { CustomEditorWidget } from '../../main/browser/custom-editors/custom-edi import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language'; import { LanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageService'; -import { Measurement, Stopwatch } from '@theia/core/lib/common'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer'; import { BasicChannel } from '@theia/core/lib/common/message-rpc/channel'; import { NotebookTypeRegistry, NotebookService, NotebookRendererMessagingService } from '@theia/notebook/lib/browser'; +import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; +import { + AbstractHostedPluginSupport, PluginContributions, PluginHost, + ALL_ACTIVATION_EVENT, isConnectionScopedBackendPlugin +} from '../common/hosted-plugin'; +import { isRemote } from '@theia/core/lib/browser/browser'; -export type PluginHost = 'frontend' | string; export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker' | 'onDebugDynamicConfigurations'; export const PluginProgressLocation = 'plugin'; -export const ALL_ACTIVATION_EVENT = '*'; @injectable() -export class HostedPluginSupport { - - protected readonly clientId = UUID.uuid4(); - - protected container: interfaces.Container; - - @inject(ILogger) - protected readonly logger: ILogger; - - @inject(HostedPluginServer) - protected readonly server: RpcProxy; +export class HostedPluginSupport extends AbstractHostedPluginSupport> { + + protected static ADDITIONAL_ACTIVATION_EVENTS_ENV = 'ADDITIONAL_ACTIVATION_EVENTS'; + protected static BUILTIN_ACTIVATION_EVENTS = [ + '*', + 'onLanguage', + 'onCommand', + 'onDebug', + 'onDebugInitialConfigurations', + 'onDebugResolve', + 'onDebugAdapterProtocolTracker', + 'onDebugDynamicConfigurations', + 'onTaskType', + 'workspaceContains', + 'onView', + 'onUri', + 'onTerminalProfile', + 'onWebviewPanel', + 'onFileSystem', + 'onCustomEditor', + 'onStartupFinished', + 'onAuthenticationRequest', + 'onNotebook', + 'onNotebookSerializer' + ]; @inject(HostedPluginWatcher) protected readonly watcher: HostedPluginWatcher; @@ -96,22 +110,12 @@ export class HostedPluginSupport { @inject(PluginContributionHandler) protected readonly contributionHandler: PluginContributionHandler; - @inject(ContributionProvider) - @named(MainPluginApiProvider) - protected readonly mainPluginApiProviders: ContributionProvider; - - @inject(PluginServer) - protected readonly pluginServer: PluginServer; - @inject(PreferenceProviderProvider) protected readonly preferenceProviderProvider: PreferenceProviderProvider; @inject(PreferenceServiceImpl) protected readonly preferenceServiceImpl: PreferenceServiceImpl; - @inject(PluginPathsService) - protected readonly pluginPathsService: PluginPathsService; - @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @@ -166,55 +170,30 @@ export class HostedPluginSupport { @inject(TerminalService) protected readonly terminalService: TerminalService; - @inject(EnvVariablesServer) - protected readonly envServer: EnvVariablesServer; - @inject(JsonSchemaStore) protected readonly jsonSchemaStore: JsonSchemaStore; @inject(PluginCustomEditorRegistry) protected readonly customEditorRegistry: PluginCustomEditorRegistry; - @inject(Stopwatch) - protected readonly stopwatch: Stopwatch; - - protected theiaReadyPromise: Promise; - - protected readonly managers = new Map(); - - protected readonly contributions = new Map(); + @inject(ApplicationServer) + protected readonly applicationServer: ApplicationServer; - protected readonly activationEvents = new Set(); - - protected readonly onDidChangePluginsEmitter = new Emitter(); - readonly onDidChangePlugins = this.onDidChangePluginsEmitter.event; - - protected readonly deferredWillStart = new Deferred(); - /** - * Resolves when the initial plugins are loaded and about to be started. - */ - get willStart(): Promise { - return this.deferredWillStart.promise; - } - - protected readonly deferredDidStart = new Deferred(); - /** - * Resolves when the initial plugins are started. - */ - get didStart(): Promise { - return this.deferredDidStart.promise; + constructor() { + super(generateUuid()); } @postConstruct() - protected init(): void { - this.theiaReadyPromise = Promise.all([this.preferenceServiceImpl.ready, this.workspaceService.roots]); + protected override init(): void { + super.init(); + this.workspaceService.onWorkspaceChanged(() => this.updateStoragePath()); const languageService = (StandaloneServices.get(ILanguageService) as LanguageService); - for (const language of languageService['_encounteredLanguages'] as Set) { + for (const language of languageService['_requestedBasicLanguages'] as Set) { this.activateByLanguage(language); } - languageService.onDidEncounterLanguage(language => this.activateByLanguage(language)); + languageService.onDidRequestBasicLanguageFeatures(language => this.activateByLanguage(language)); this.commands.onWillExecuteCommand(event => this.ensureCommandHandlerRegistration(event)); this.debugSessionManager.onWillStartDebugSession(event => this.ensureDebugActivation(event)); this.debugSessionManager.onWillResolveDebugConfiguration(event => this.ensureDebugActivation(event, 'onDebugResolve', event.debugType)); @@ -230,7 +209,8 @@ export class HostedPluginSupport { this.notebookRendererMessagingService.onWillActivateRenderer(rendererId => this.activateByNotebookRenderer(rendererId)); this.widgets.onDidCreateWidget(({ factoryId, widget }) => { - if ((factoryId === WebviewWidget.FACTORY_ID || factoryId === CustomEditorWidget.FACTORY_ID) && widget instanceof WebviewWidget) { + // note: state restoration of custom editors is handled in `PluginCustomEditorRegistry.init` + if (factoryId === WebviewWidget.FACTORY_ID && widget instanceof WebviewWidget) { const storeState = widget.storeState.bind(widget); const restoreState = widget.restoreState.bind(widget); @@ -253,242 +233,50 @@ export class HostedPluginSupport { }); } - get plugins(): PluginMetadata[] { - const plugins: PluginMetadata[] = []; - this.contributions.forEach(contributions => plugins.push(contributions.plugin.metadata)); - return plugins; + protected createTheiaReadyPromise(): Promise { + return Promise.all([this.preferenceServiceImpl.ready, this.workspaceService.roots]); } - getPlugin(id: PluginIdentifiers.UnversionedId): DeployedPlugin | undefined { - const contributions = this.contributions.get(id); - return contributions && contributions.plugin; + protected override runOperation(operation: () => Promise): Promise { + return this.progressService.withProgress('', PluginProgressLocation, () => this.doLoad()); } - /** do not call it, except from the plugin frontend contribution */ - onStart(container: interfaces.Container): void { - this.container = container; - this.load(); + protected override afterStart(): void { this.watcher.onDidDeploy(() => this.load()); this.server.onDidOpenConnection(() => this.load()); } - protected loadQueue: Promise = Promise.resolve(undefined); - load = debounce(() => this.loadQueue = this.loadQueue.then(async () => { - try { - await this.progressService.withProgress('', PluginProgressLocation, () => this.doLoad()); - } catch (e) { - console.error('Failed to load plugins:', e); - } - }), 50, { leading: true }); + // Only load connection-scoped plugins + protected acceptPlugin(plugin: DeployedPlugin): boolean { + return isConnectionScopedBackendPlugin(plugin); + } + + protected override async beforeSyncPlugins(toDisconnect: DisposableCollection): Promise { + await super.beforeSyncPlugins(toDisconnect); - protected async doLoad(): Promise { - const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ })); toDisconnect.push(Disposable.create(() => this.preserveWebviews())); this.server.onDidCloseConnection(() => toDisconnect.dispose()); + } - // process empty plugins as well in order to properly remove stale plugin widgets - await this.syncPlugins(); - - // it has to be resolved before awaiting layout is initialized - // otherwise clients can hang forever in the initialization phase - this.deferredWillStart.resolve(); - + protected override async beforeLoadContributions(toDisconnect: DisposableCollection): Promise { // make sure that the previous state, including plugin widgets, is restored // and core layout is initialized, i.e. explorer, scm, debug views are already added to the shell // but shell is not yet revealed await this.appState.reachedState('initialized_layout'); + } - if (toDisconnect.disposed) { - // if disconnected then don't try to load plugin contributions - return; - } - const contributionsByHost = this.loadContributions(toDisconnect); - + protected override async afterLoadContributions(toDisconnect: DisposableCollection): Promise { await this.viewRegistry.initWidgets(); // remove restored plugin widgets which were not registered by contributions this.viewRegistry.removeStaleWidgets(); - await this.theiaReadyPromise; - - if (toDisconnect.disposed) { - // if disconnected then don't try to init plugin code and dynamic contributions - return; - } - await this.startPlugins(contributionsByHost, toDisconnect); - - this.deferredDidStart.resolve(); - } - - /** - * Sync loaded and deployed plugins: - * - undeployed plugins are unloaded - * - newly deployed plugins are initialized - */ - protected async syncPlugins(): Promise { - let initialized = 0; - const waitPluginsMeasurement = this.measure('waitForDeployment'); - let syncPluginsMeasurement: Measurement | undefined; - - const toUnload = new Set(this.contributions.keys()); - let didChangeInstallationStatus = false; - try { - const newPluginIds: PluginIdentifiers.VersionedId[] = []; - const [deployedPluginIds, uninstalledPluginIds] = await Promise.all([this.server.getDeployedPluginIds(), this.server.getUninstalledPluginIds()]); - waitPluginsMeasurement.log('Waiting for backend deployment'); - syncPluginsMeasurement = this.measure('syncPlugins'); - for (const versionedId of deployedPluginIds) { - const unversionedId = PluginIdentifiers.unversionedFromVersioned(versionedId); - toUnload.delete(unversionedId); - if (!this.contributions.has(unversionedId)) { - newPluginIds.push(versionedId); - } - } - for (const pluginId of toUnload) { - this.contributions.get(pluginId)?.dispose(); - } - for (const versionedId of uninstalledPluginIds) { - const plugin = this.getPlugin(PluginIdentifiers.unversionedFromVersioned(versionedId)); - if (plugin && PluginIdentifiers.componentsToVersionedId(plugin.metadata.model) === versionedId && !plugin.metadata.outOfSync) { - plugin.metadata.outOfSync = didChangeInstallationStatus = true; - } - } - for (const contribution of this.contributions.values()) { - if (contribution.plugin.metadata.outOfSync && !uninstalledPluginIds.includes(PluginIdentifiers.componentsToVersionedId(contribution.plugin.metadata.model))) { - contribution.plugin.metadata.outOfSync = false; - didChangeInstallationStatus = true; - } - } - if (newPluginIds.length) { - const plugins = await this.server.getDeployedPlugins({ pluginIds: newPluginIds }); - for (const plugin of plugins) { - const pluginId = PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model); - const contributions = new PluginContributions(plugin); - this.contributions.set(pluginId, contributions); - contributions.push(Disposable.create(() => this.contributions.delete(pluginId))); - initialized++; - } - } - } finally { - if (initialized || toUnload.size || didChangeInstallationStatus) { - this.onDidChangePluginsEmitter.fire(undefined); - } - - if (!syncPluginsMeasurement) { - // await didn't complete normally - waitPluginsMeasurement.error('Backend deployment failed.'); - } - } - - syncPluginsMeasurement?.log(`Sync of ${this.getPluginCount(initialized)}`); - } - - /** - * Always synchronous in order to simplify handling disconnections. - * @throws never - */ - protected loadContributions(toDisconnect: DisposableCollection): Map { - let loaded = 0; - const loadPluginsMeasurement = this.measure('loadPlugins'); - - const hostContributions = new Map(); - console.log(`[${this.clientId}] Loading plugin contributions`); - for (const contributions of this.contributions.values()) { - const plugin = contributions.plugin.metadata; - const pluginId = plugin.model.id; - - if (contributions.state === PluginContributions.State.INITIALIZING) { - contributions.state = PluginContributions.State.LOADING; - contributions.push(Disposable.create(() => console.log(`[${pluginId}]: Unloaded plugin.`))); - contributions.push(this.contributionHandler.handleContributions(this.clientId, contributions.plugin)); - contributions.state = PluginContributions.State.LOADED; - console.debug(`[${this.clientId}][${pluginId}]: Loaded contributions.`); - loaded++; - } - - if (contributions.state === PluginContributions.State.LOADED) { - contributions.state = PluginContributions.State.STARTING; - const host = plugin.model.entryPoint.frontend ? 'frontend' : plugin.host; - const dynamicContributions = hostContributions.get(host) || []; - dynamicContributions.push(contributions); - hostContributions.set(host, dynamicContributions); - toDisconnect.push(Disposable.create(() => { - contributions!.state = PluginContributions.State.LOADED; - console.debug(`[${this.clientId}][${pluginId}]: Disconnected.`); - })); - } - } - - loadPluginsMeasurement.log(`Load contributions of ${this.getPluginCount(loaded)}`); - - return hostContributions; } - protected async startPlugins(contributionsByHost: Map, toDisconnect: DisposableCollection): Promise { - let started = 0; - const startPluginsMeasurement = this.measure('startPlugins'); - - const [hostLogPath, hostStoragePath, hostGlobalStoragePath] = await Promise.all([ - this.pluginPathsService.getHostLogPath(), - this.getStoragePath(), - this.getHostGlobalStoragePath() - ]); - - if (toDisconnect.disposed) { - return; - } - - const thenable: Promise[] = []; - const configStorage: ConfigStorage = { - hostLogPath, - hostStoragePath, - hostGlobalStoragePath - }; - - for (const [host, hostContributions] of contributionsByHost) { - // do not start plugins for electron browser - if (host === 'frontend' && environment.electron.is()) { - continue; - } - - const manager = await this.obtainManager(host, hostContributions, toDisconnect); - if (!manager) { - continue; - } - - const plugins = hostContributions.map(contributions => contributions.plugin.metadata); - thenable.push((async () => { - try { - const activationEvents = [...this.activationEvents]; - await manager.$start({ plugins, configStorage, activationEvents }); - if (toDisconnect.disposed) { - return; - } - console.log(`[${this.clientId}] Starting plugins.`); - for (const contributions of hostContributions) { - started++; - const plugin = contributions.plugin; - const id = plugin.metadata.model.id; - contributions.state = PluginContributions.State.STARTED; - console.debug(`[${this.clientId}][${id}]: Started plugin.`); - toDisconnect.push(contributions.push(Disposable.create(() => { - console.debug(`[${this.clientId}][${id}]: Stopped plugin.`); - manager.$stop(id); - }))); - - this.activateByWorkspaceContains(manager, plugin); - } - } catch (e) { - console.error(`Failed to start plugins for '${host}' host`, e); - } - })()); - } - - await Promise.all(thenable); - await this.activateByEvent('onStartupFinished'); - if (toDisconnect.disposed) { - return; - } + protected handleContributions(plugin: DeployedPlugin): Disposable { + return this.contributionHandler.handleContributions(this.clientId, plugin); + } - startPluginsMeasurement.log(`Start of ${this.getPluginCount(started)}`); + protected override handlePluginStarted(manager: PluginManagerExt, plugin: DeployedPlugin): void { + this.activateByWorkspaceContains(manager, plugin); } protected async obtainManager(host: string, hostContributions: PluginContributions[], toDisconnect: DisposableCollection): Promise { @@ -519,6 +307,16 @@ export class HostedPluginSupport { } const isElectron = environment.electron.is(); + + const supportedActivationEvents = [...HostedPluginSupport.BUILTIN_ACTIVATION_EVENTS]; + const [additionalActivationEvents, appRoot] = await Promise.all([ + this.envServer.getValue(HostedPluginSupport.ADDITIONAL_ACTIVATION_EVENTS_ENV), + this.applicationServer.getApplicationRoot() + ]); + if (additionalActivationEvents && additionalActivationEvents.value) { + additionalActivationEvents.value.split(',').forEach(event => supportedActivationEvents.push(event)); + } + await manager.$init({ preferences: getPreferences(this.preferenceProviderProvider, this.workspaceService.tryGetRoots()), globalState, @@ -529,18 +327,23 @@ export class HostedPluginSupport { shell: defaultShell, uiKind: isElectron ? UIKind.Desktop : UIKind.Web, appName: FrontendApplicationConfigProvider.get().applicationName, - appHost: isElectron ? 'desktop' : 'web' // TODO: 'web' could be the embedder's name, e.g. 'github.dev' + appHost: isElectron ? 'desktop' : 'web', // TODO: 'web' could be the embedder's name, e.g. 'github.dev' + appRoot, + appUriScheme: FrontendApplicationConfigProvider.get().electron.uriScheme }, extApi, webview: { webviewResourceRoot, webviewCspSource }, - jsonValidation + jsonValidation, + pluginKind: isRemote ? ExtensionKind.Workspace : ExtensionKind.UI, + supportedActivationEvents }); if (toDisconnect.disposed) { return undefined; } + this.activationEvents.forEach(event => manager!.$activateByEvent(event)); } return manager; } @@ -601,14 +404,6 @@ export class HostedPluginSupport { return globalStorageFolderFsPath; } - async activateByEvent(activationEvent: string): Promise { - if (this.activationEvents.has(activationEvent)) { - return; - } - this.activationEvents.add(activationEvent); - await Promise.all(Array.from(this.managers.values(), manager => manager.$activateByEvent(activationEvent))); - } - async activateByViewContainer(viewContainerId: string): Promise { await Promise.all(this.viewRegistry.getContainerViews(viewContainerId).map(viewId => this.activateByView(viewId))); } @@ -618,9 +413,14 @@ export class HostedPluginSupport { } async activateByLanguage(languageId: string): Promise { + await this.activateByEvent('onLanguage'); await this.activateByEvent(`onLanguage:${languageId}`); } + async activateByUri(scheme: string, authority: string): Promise { + await this.activateByEvent(`onUri:${scheme}://${authority}`); + } + async activateByCommand(commandId: string): Promise { await this.activateByEvent(`onCommand:${commandId}`); } @@ -654,7 +454,12 @@ export class HostedPluginSupport { } protected ensureFileSystemActivation(event: FileSystemProviderActivationEvent): void { - event.waitUntil(this.activateByFileSystem(event)); + event.waitUntil(this.activateByFileSystem(event).then(() => { + if (!this.fileService.hasProvider(event.scheme)) { + return waitForEvent(Event.filter(this.fileService.onDidChangeFileSystemProviderRegistrations, + ({ added, scheme }) => added && scheme === event.scheme), 3000); + } + })); } protected ensureCommandHandlerRegistration(event: WillExecuteCommandEvent): void { @@ -763,22 +568,6 @@ export class HostedPluginSupport { } } - async activatePlugin(id: string): Promise { - const activation = []; - for (const manager of this.managers.values()) { - activation.push(manager.$activatePlugin(id)); - } - await Promise.all(activation); - } - - protected measure(name: string): Measurement { - return this.stopwatch.start(name, { context: this.clientId }); - } - - protected getPluginCount(plugins: number): string { - return `${plugins} plugin${plugins === 1 ? '' : 's'}`; - } - protected readonly webviewsToRestore = new Map(); protected readonly webviewRevivers = new Map Promise>(); @@ -844,22 +633,3 @@ export class HostedPluginSupport { } } - -export class PluginContributions extends DisposableCollection { - constructor( - readonly plugin: DeployedPlugin - ) { - super(); - } - state: PluginContributions.State = PluginContributions.State.INITIALIZING; -} - -export namespace PluginContributions { - export enum State { - INITIALIZING = 0, - LOADING = 1, - LOADED = 2, - STARTING = 3, - STARTED = 4 - } -} diff --git a/packages/plugin-ext/src/hosted/browser/worker/debug-stub.ts b/packages/plugin-ext/src/hosted/browser/worker/debug-stub.ts index 9dd3e48b64440..f37b9b0a2f431 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/debug-stub.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/debug-stub.ts @@ -15,12 +15,13 @@ // ***************************************************************************** // eslint-disable-next-line @theia/runtime-import-check +import { interfaces } from '@theia/core/shared/inversify'; import { DebugExtImpl } from '../../../plugin/debug/debug-ext'; -import { RPCProtocol } from '../../../common/rpc-protocol'; /* eslint-disable @typescript-eslint/no-explicit-any */ -export function createDebugExtStub(rpc: RPCProtocol): DebugExtImpl { - return new Proxy(new DebugExtImpl(rpc), { +export function createDebugExtStub(container: interfaces.Container): DebugExtImpl { + const delegate = container.get(DebugExtImpl); + return new Proxy(delegate, { apply: function (target, that, args): void { console.error('Debug API works only in plugin container'); } diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-env-ext.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-env-ext.ts index 993827d6bd50c..d94a3b3ba51a7 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-env-ext.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-env-ext.ts @@ -14,24 +14,23 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { injectable } from '@theia/core/shared/inversify'; import { EnvExtImpl } from '../../../plugin/env'; -import { RPCProtocol } from '../../../common/rpc-protocol'; /** * Worker specific implementation not returning any FileSystem details * Extending the common class */ +@injectable() export class WorkerEnvExtImpl extends EnvExtImpl { - constructor(rpc: RPCProtocol) { - super(rpc); + constructor() { + super(); } - /** - * Throw error for app-root as there is no filesystem in worker context - */ - get appRoot(): string { - throw new Error('There is no app root in worker context'); + override get appRoot(): string { + // The documentation indicates that this should be an empty string + return ''; } get isNewAppInstall(): boolean { diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts index caae9776e2f18..3739987040f1a 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts @@ -15,13 +15,12 @@ // ***************************************************************************** // eslint-disable-next-line import/no-extraneous-dependencies import 'reflect-metadata'; -import { BasicChannel } from '@theia/core/lib/common/message-rpc/channel'; -import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer'; +import { Container } from '@theia/core/shared/inversify'; import * as theia from '@theia/plugin'; -import { emptyPlugin, MAIN_RPC_CONTEXT, Plugin, TerminalServiceExt } from '../../../common/plugin-api-rpc'; +import { emptyPlugin, MAIN_RPC_CONTEXT, Plugin } from '../../../common/plugin-api-rpc'; import { ExtPluginApi } from '../../../common/plugin-ext-api-contribution'; import { getPluginId, PluginMetadata } from '../../../common/plugin-protocol'; -import { RPCProtocolImpl } from '../../../common/rpc-protocol'; +import { RPCProtocol } from '../../../common/rpc-protocol'; import { ClipboardExt } from '../../../plugin/clipboard-ext'; import { EditorsAndDocumentsExtImpl } from '../../../plugin/editors-and-documents'; import { MessageRegistryExt } from '../../../plugin/message-registry'; @@ -29,14 +28,13 @@ import { createAPIFactory } from '../../../plugin/plugin-context'; import { PluginManagerExtImpl } from '../../../plugin/plugin-manager'; import { KeyValueStorageProxy } from '../../../plugin/plugin-storage'; import { PreferenceRegistryExtImpl } from '../../../plugin/preference-registry'; -import { SecretsExtImpl } from '../../../plugin/secrets-ext'; -import { TerminalServiceExtImpl } from '../../../plugin/terminal-ext'; import { WebviewsExtImpl } from '../../../plugin/webviews'; import { WorkspaceExtImpl } from '../../../plugin/workspace'; -import { createDebugExtStub } from './debug-stub'; import { loadManifest } from './plugin-manifest-loader'; -import { WorkerEnvExtImpl } from './worker-env-ext'; +import { EnvExtImpl } from '../../../plugin/env'; +import { DebugExtImpl } from '../../../plugin/debug/debug-ext'; import { LocalizationExtImpl } from '../../../plugin/localization-ext'; +import pluginHostModule from './worker-plugin-module'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const ctx = self as any; @@ -44,136 +42,126 @@ const ctx = self as any; const pluginsApiImpl = new Map(); const pluginsModulesNames = new Map(); -const channel = new BasicChannel(() => { - const writeBuffer = new Uint8ArrayWriteBuffer(); - writeBuffer.onCommit(buffer => { - ctx.postMessage(buffer); - }); - return writeBuffer; -}); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -addEventListener('message', (message: any) => { - channel.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(message.data)); -}); - -const rpc = new RPCProtocolImpl(channel); - const scripts = new Set(); function initialize(contextPath: string, pluginMetadata: PluginMetadata): void { - const path = '/context/' + contextPath; + const path = './context/' + contextPath; if (!scripts.has(path)) { ctx.importScripts(path); scripts.add(path); } } -const envExt = new WorkerEnvExtImpl(rpc); -const storageProxy = new KeyValueStorageProxy(rpc); -const editorsAndDocuments = new EditorsAndDocumentsExtImpl(rpc); -const messageRegistryExt = new MessageRegistryExt(rpc); -const workspaceExt = new WorkspaceExtImpl(rpc, editorsAndDocuments, messageRegistryExt); -const preferenceRegistryExt = new PreferenceRegistryExtImpl(rpc, workspaceExt); -const debugExt = createDebugExtStub(rpc); -const clipboardExt = new ClipboardExt(rpc); -const webviewExt = new WebviewsExtImpl(rpc, workspaceExt); -const secretsExt = new SecretsExtImpl(rpc); -const localizationExt = new LocalizationExtImpl(rpc); -const terminalService: TerminalServiceExt = new TerminalServiceExtImpl(rpc); - -const pluginManager = new PluginManagerExtImpl({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - loadPlugin(plugin: Plugin): any { - if (plugin.pluginPath) { - if (isElectron()) { - ctx.importScripts(plugin.pluginPath); - } else { - if (plugin.lifecycle.frontendModuleName) { - // Set current module name being imported - ctx.frontendModuleName = plugin.lifecycle.frontendModuleName; - } - ctx.importScripts('/hostedPlugin/' + getPluginId(plugin.model) + '/' + plugin.pluginPath); - } - } +const container = new Container(); +container.load(pluginHostModule); + +const rpc: RPCProtocol = container.get(RPCProtocol); +const pluginManager = container.get(PluginManagerExtImpl); +pluginManager.setPluginHost({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + loadPlugin(plugin: Plugin): any { + if (plugin.pluginPath) { + if (isElectron()) { + ctx.importScripts(plugin.pluginPath); + } else { + if (plugin.lifecycle.frontendModuleName) { + // Set current module name being imported + ctx.frontendModuleName = plugin.lifecycle.frontendModuleName; + } - if (plugin.lifecycle.frontendModuleName) { - if (!ctx[plugin.lifecycle.frontendModuleName]) { - console.error(`WebWorker: Cannot start plugin "${plugin.model.name}". Frontend plugin not found: "${plugin.lifecycle.frontendModuleName}"`); - return; + ctx.importScripts('./hostedPlugin/' + getPluginId(plugin.model) + '/' + plugin.pluginPath); + } } - return ctx[plugin.lifecycle.frontendModuleName]; - } - }, - async init(rawPluginData: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> { - const result: Plugin[] = []; - const foreign: Plugin[] = []; - // Process the plugins concurrently, making sure to keep the order. - const plugins = await Promise.all<{ - /** Where to push the plugin: `result` or `foreign` */ - target: Plugin[], - plugin: Plugin - }>(rawPluginData.map(async plg => { - const pluginModel = plg.model; - const pluginLifecycle = plg.lifecycle; - if (pluginModel.entryPoint!.frontend) { - let frontendInitPath = pluginLifecycle.frontendInitPath; - if (frontendInitPath) { - initialize(frontendInitPath, plg); - } else { - frontendInitPath = ''; + + if (plugin.lifecycle.frontendModuleName) { + if (!ctx[plugin.lifecycle.frontendModuleName]) { + console.error(`WebWorker: Cannot start plugin "${plugin.model.name}". Frontend plugin not found: "${plugin.lifecycle.frontendModuleName}"`); + return; } - const rawModel = await loadManifest(pluginModel); - const plugin: Plugin = { - pluginPath: pluginModel.entryPoint.frontend!, - pluginFolder: pluginModel.packagePath, - pluginUri: pluginModel.packageUri, - model: pluginModel, - lifecycle: pluginLifecycle, - rawModel, - isUnderDevelopment: !!plg.isUnderDevelopment - }; - const apiImpl = apiFactory(plugin); - pluginsApiImpl.set(plugin.model.id, apiImpl); - pluginsModulesNames.set(plugin.lifecycle.frontendModuleName!, plugin); - return { target: result, plugin }; - } else { - return { - target: foreign, - plugin: { - pluginPath: pluginModel.entryPoint.backend, + return ctx[plugin.lifecycle.frontendModuleName]; + } + }, + async init(rawPluginData: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> { + const result: Plugin[] = []; + const foreign: Plugin[] = []; + // Process the plugins concurrently, making sure to keep the order. + const plugins = await Promise.all<{ + /** Where to push the plugin: `result` or `foreign` */ + target: Plugin[], + plugin: Plugin + }>(rawPluginData.map(async plg => { + const pluginModel = plg.model; + const pluginLifecycle = plg.lifecycle; + if (pluginModel.entryPoint!.frontend) { + let frontendInitPath = pluginLifecycle.frontendInitPath; + if (frontendInitPath) { + initialize(frontendInitPath, plg); + } else { + frontendInitPath = ''; + } + const rawModel = await loadManifest(pluginModel); + const plugin: Plugin = { + pluginPath: pluginModel.entryPoint.frontend!, pluginFolder: pluginModel.packagePath, pluginUri: pluginModel.packageUri, model: pluginModel, lifecycle: pluginLifecycle, - get rawModel(): never { - throw new Error('not supported'); - }, + rawModel, isUnderDevelopment: !!plg.isUnderDevelopment - } - }; - } - })); - // Collect the ordered plugins and insert them in the target array: - for (const { target, plugin } of plugins) { - target.push(plugin); - } - return [result, foreign]; - }, - initExtApi(extApi: ExtPluginApi[]): void { - for (const api of extApi) { - try { - if (api.frontendExtApi) { - ctx.importScripts(api.frontendExtApi.initPath); - ctx[api.frontendExtApi.initVariable][api.frontendExtApi.initFunction](rpc, pluginsModulesNames); + }; + const apiImpl = apiFactory(plugin); + pluginsApiImpl.set(plugin.model.id, apiImpl); + pluginsModulesNames.set(plugin.lifecycle.frontendModuleName!, plugin); + return { target: result, plugin }; + } else { + return { + target: foreign, + plugin: { + pluginPath: pluginModel.entryPoint.backend, + pluginFolder: pluginModel.packagePath, + pluginUri: pluginModel.packageUri, + model: pluginModel, + lifecycle: pluginLifecycle, + get rawModel(): never { + throw new Error('not supported'); + }, + isUnderDevelopment: !!plg.isUnderDevelopment + } + }; } + })); + // Collect the ordered plugins and insert them in the target array: + for (const { target, plugin } of plugins) { + target.push(plugin); + } + return [result, foreign]; + }, + initExtApi(extApi: ExtPluginApi[]): void { + for (const api of extApi) { + try { + if (api.frontendExtApi) { + ctx.importScripts(api.frontendExtApi.initPath); + ctx[api.frontendExtApi.initVariable][api.frontendExtApi.initFunction](rpc, pluginsModulesNames); + } - } catch (e) { - console.error(e); + } catch (e) { + console.error(e); + } } } - } -}, envExt, terminalService, storageProxy, secretsExt, preferenceRegistryExt, webviewExt, localizationExt, rpc); + }); + +const envExt = container.get(EnvExtImpl); +const debugExt = container.get(DebugExtImpl); +const preferenceRegistryExt = container.get(PreferenceRegistryExtImpl); +const editorsAndDocuments = container.get(EditorsAndDocumentsExtImpl); +const workspaceExt = container.get(WorkspaceExtImpl); +const messageRegistryExt = container.get(MessageRegistryExt); +const clipboardExt = container.get(ClipboardExt); +const webviewExt = container.get(WebviewsExtImpl); +const localizationExt = container.get(LocalizationExtImpl); +const storageProxy = container.get(KeyValueStorageProxy); const apiFactory = createAPIFactory( rpc, diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-plugin-module.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-plugin-module.ts new file mode 100644 index 0000000000000..155575aaff93a --- /dev/null +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-plugin-module.ts @@ -0,0 +1,80 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +// eslint-disable-next-line import/no-extraneous-dependencies +import 'reflect-metadata'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { BasicChannel } from '@theia/core/lib/common/message-rpc/channel'; +import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer'; +import { LocalizationExt } from '../../../common/plugin-api-rpc'; +import { RPCProtocol, RPCProtocolImpl } from '../../../common/rpc-protocol'; +import { ClipboardExt } from '../../../plugin/clipboard-ext'; +import { EditorsAndDocumentsExtImpl } from '../../../plugin/editors-and-documents'; +import { MessageRegistryExt } from '../../../plugin/message-registry'; +import { MinimalTerminalServiceExt, PluginManagerExtImpl } from '../../../plugin/plugin-manager'; +import { InternalStorageExt, KeyValueStorageProxy } from '../../../plugin/plugin-storage'; +import { PreferenceRegistryExtImpl } from '../../../plugin/preference-registry'; +import { InternalSecretsExt, SecretsExtImpl } from '../../../plugin/secrets-ext'; +import { TerminalServiceExtImpl } from '../../../plugin/terminal-ext'; +import { WebviewsExtImpl } from '../../../plugin/webviews'; +import { WorkspaceExtImpl } from '../../../plugin/workspace'; +import { createDebugExtStub } from './debug-stub'; +import { EnvExtImpl } from '../../../plugin/env'; +import { WorkerEnvExtImpl } from './worker-env-ext'; +import { DebugExtImpl } from '../../../plugin/debug/debug-ext'; +import { LocalizationExtImpl } from '../../../plugin/localization-ext'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const ctx = self as any; + +export default new ContainerModule(bind => { + const channel = new BasicChannel(() => { + const writeBuffer = new Uint8ArrayWriteBuffer(); + writeBuffer.onCommit(buffer => { + ctx.postMessage(buffer); + }); + return writeBuffer; + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addEventListener('message', (message: any) => { + channel.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(message.data)); + }); + + const rpc = new RPCProtocolImpl(channel); + + bind(RPCProtocol).toConstantValue(rpc); + + bind(PluginManagerExtImpl).toSelf().inSingletonScope(); + bind(EnvExtImpl).to(WorkerEnvExtImpl).inSingletonScope(); + bind(LocalizationExtImpl).toSelf().inSingletonScope(); + bind(LocalizationExt).toService(LocalizationExtImpl); + bind(KeyValueStorageProxy).toSelf().inSingletonScope(); + bind(InternalStorageExt).toService(KeyValueStorageProxy); + bind(SecretsExtImpl).toSelf().inSingletonScope(); + bind(InternalSecretsExt).toService(SecretsExtImpl); + bind(PreferenceRegistryExtImpl).toSelf().inSingletonScope(); + bind(DebugExtImpl).toDynamicValue(({ container }) => { + const child = container.createChild(); + child.bind(DebugExtImpl).toSelf(); + return createDebugExtStub(child); + }).inSingletonScope(); + bind(EditorsAndDocumentsExtImpl).toSelf().inSingletonScope(); + bind(WorkspaceExtImpl).toSelf().inSingletonScope(); + bind(MessageRegistryExt).toSelf().inSingletonScope(); + bind(ClipboardExt).toSelf().inSingletonScope(); + bind(WebviewsExtImpl).toSelf().inSingletonScope(); + bind(TerminalServiceExtImpl).toSelf().inSingletonScope(); + bind(MinimalTerminalServiceExt).toService(TerminalServiceExtImpl); +}); diff --git a/packages/plugin-ext/src/hosted/common/hosted-plugin.ts b/packages/plugin-ext/src/hosted/common/hosted-plugin.ts new file mode 100644 index 0000000000000..422bd76a4fe1a --- /dev/null +++ b/packages/plugin-ext/src/hosted/common/hosted-plugin.ts @@ -0,0 +1,456 @@ +// ***************************************************************************** +// Copyright (C) 2018 Red Hat, Inc. 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// some code copied and modified from https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import debounce = require('@theia/core/shared/lodash.debounce'); +import { injectable, inject, interfaces, named, postConstruct } from '@theia/core/shared/inversify'; +import { PluginMetadata, HostedPluginServer, DeployedPlugin, PluginServer, PluginIdentifiers } from '../../common/plugin-protocol'; +import { AbstractPluginManagerExt, ConfigStorage } from '../../common/plugin-api-rpc'; +import { + Disposable, DisposableCollection, Emitter, + ILogger, ContributionProvider, + RpcProxy +} from '@theia/core'; +import { MainPluginApiProvider } from '../../common/plugin-ext-api-contribution'; +import { PluginPathsService } from '../../main/common/plugin-paths-protocol'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { environment } from '@theia/core/shared/@theia/application-package/lib/environment'; +import { Measurement, Stopwatch } from '@theia/core/lib/common'; + +export type PluginHost = 'frontend' | string; + +export const ALL_ACTIVATION_EVENT = '*'; + +export function isConnectionScopedBackendPlugin(plugin: DeployedPlugin): boolean { + const entryPoint = plugin.metadata.model.entryPoint; + + // A plugin doesn't have to have any entry-point if it doesn't need the activation handler, + // in which case it's assumed to be a backend plugin. + return !entryPoint.headless || !!entryPoint.backend; +} + +@injectable() +export abstract class AbstractHostedPluginSupport, HPS extends HostedPluginServer | RpcProxy> { + + protected container: interfaces.Container; + + @inject(ILogger) + protected readonly logger: ILogger; + + @inject(HostedPluginServer) + protected readonly server: HPS; + + @inject(ContributionProvider) + @named(MainPluginApiProvider) + protected readonly mainPluginApiProviders: ContributionProvider; + + @inject(PluginServer) + protected readonly pluginServer: PluginServer; + + @inject(PluginPathsService) + protected readonly pluginPathsService: PluginPathsService; + + @inject(EnvVariablesServer) + protected readonly envServer: EnvVariablesServer; + + @inject(Stopwatch) + protected readonly stopwatch: Stopwatch; + + protected theiaReadyPromise: Promise; + + protected readonly managers = new Map(); + + protected readonly contributions = new Map(); + + protected readonly activationEvents = new Set(); + + protected readonly onDidChangePluginsEmitter = new Emitter(); + readonly onDidChangePlugins = this.onDidChangePluginsEmitter.event; + + protected readonly deferredWillStart = new Deferred(); + /** + * Resolves when the initial plugins are loaded and about to be started. + */ + get willStart(): Promise { + return this.deferredWillStart.promise; + } + + protected readonly deferredDidStart = new Deferred(); + /** + * Resolves when the initial plugins are started. + */ + get didStart(): Promise { + return this.deferredDidStart.promise; + } + + constructor(protected readonly clientId: string) { } + + @postConstruct() + protected init(): void { + this.theiaReadyPromise = this.createTheiaReadyPromise(); + } + + protected abstract createTheiaReadyPromise(): Promise; + + get plugins(): PluginMetadata[] { + const plugins: PluginMetadata[] = []; + this.contributions.forEach(contributions => plugins.push(contributions.plugin.metadata)); + return plugins; + } + + getPlugin(id: PluginIdentifiers.UnversionedId): DeployedPlugin | undefined { + const contributions = this.contributions.get(id); + return contributions && contributions.plugin; + } + + /** do not call it, except from the plugin frontend contribution */ + onStart(container: interfaces.Container): void { + this.container = container; + this.load(); + this.afterStart(); + } + + protected afterStart(): void { + // Nothing to do in the abstract + } + + protected loadQueue: Promise = Promise.resolve(undefined); + load = debounce(() => this.loadQueue = this.loadQueue.then(async () => { + try { + await this.runOperation(() => this.doLoad()); + } catch (e) { + console.error('Failed to load plugins:', e); + } + }), 50, { leading: true }); + + protected runOperation(operation: () => Promise): Promise { + return operation(); + } + + protected async doLoad(): Promise { + const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ })); + + await this.beforeSyncPlugins(toDisconnect); + + // process empty plugins as well in order to properly remove stale plugin widgets + await this.syncPlugins(); + + // it has to be resolved before awaiting layout is initialized + // otherwise clients can hang forever in the initialization phase + this.deferredWillStart.resolve(); + + await this.beforeLoadContributions(toDisconnect); + + if (toDisconnect.disposed) { + // if disconnected then don't try to load plugin contributions + return; + } + const contributionsByHost = this.loadContributions(toDisconnect); + + await this.afterLoadContributions(toDisconnect); + + await this.theiaReadyPromise; + if (toDisconnect.disposed) { + // if disconnected then don't try to init plugin code and dynamic contributions + return; + } + await this.startPlugins(contributionsByHost, toDisconnect); + + this.deferredDidStart.resolve(); + } + + protected beforeSyncPlugins(toDisconnect: DisposableCollection): Promise { + // Nothing to do in the abstract + return Promise.resolve(); + } + + protected beforeLoadContributions(toDisconnect: DisposableCollection): Promise { + // Nothing to do in the abstract + return Promise.resolve(); + } + + protected afterLoadContributions(toDisconnect: DisposableCollection): Promise { + // Nothing to do in the abstract + return Promise.resolve(); + } + + /** + * Sync loaded and deployed plugins: + * - undeployed plugins are unloaded + * - newly deployed plugins are initialized + */ + protected async syncPlugins(): Promise { + let initialized = 0; + const waitPluginsMeasurement = this.measure('waitForDeployment'); + let syncPluginsMeasurement: Measurement | undefined; + + const toUnload = new Set(this.contributions.keys()); + let didChangeInstallationStatus = false; + try { + const newPluginIds: PluginIdentifiers.VersionedId[] = []; + const [deployedPluginIds, uninstalledPluginIds] = await Promise.all([this.server.getDeployedPluginIds(), this.server.getUninstalledPluginIds()]); + waitPluginsMeasurement.log('Waiting for backend deployment'); + syncPluginsMeasurement = this.measure('syncPlugins'); + for (const versionedId of deployedPluginIds) { + const unversionedId = PluginIdentifiers.unversionedFromVersioned(versionedId); + toUnload.delete(unversionedId); + if (!this.contributions.has(unversionedId)) { + newPluginIds.push(versionedId); + } + } + for (const pluginId of toUnload) { + this.contributions.get(pluginId)?.dispose(); + } + for (const versionedId of uninstalledPluginIds) { + const plugin = this.getPlugin(PluginIdentifiers.unversionedFromVersioned(versionedId)); + if (plugin && PluginIdentifiers.componentsToVersionedId(plugin.metadata.model) === versionedId && !plugin.metadata.outOfSync) { + plugin.metadata.outOfSync = didChangeInstallationStatus = true; + } + } + for (const contribution of this.contributions.values()) { + if (contribution.plugin.metadata.outOfSync && !uninstalledPluginIds.includes(PluginIdentifiers.componentsToVersionedId(contribution.plugin.metadata.model))) { + contribution.plugin.metadata.outOfSync = false; + didChangeInstallationStatus = true; + } + } + if (newPluginIds.length) { + const deployedPlugins = await this.server.getDeployedPlugins({ pluginIds: newPluginIds }); + + const plugins: DeployedPlugin[] = []; + for (const plugin of deployedPlugins) { + const accepted = this.acceptPlugin(plugin); + if (typeof accepted === 'object') { + plugins.push(accepted); + } else if (accepted) { + plugins.push(plugin); + } + } + + for (const plugin of plugins) { + const pluginId = PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model); + const contributions = new PluginContributions(plugin); + this.contributions.set(pluginId, contributions); + contributions.push(Disposable.create(() => this.contributions.delete(pluginId))); + initialized++; + } + } + } finally { + if (initialized || toUnload.size || didChangeInstallationStatus) { + this.onDidChangePluginsEmitter.fire(undefined); + } + + if (!syncPluginsMeasurement) { + // await didn't complete normally + waitPluginsMeasurement.error('Backend deployment failed.'); + } + } + if (initialized > 0) { + // Only log sync measurement if there are were plugins to sync. + syncPluginsMeasurement?.log(`Sync of ${this.getPluginCount(initialized)}`); + } else { + syncPluginsMeasurement?.stop(); + } + } + + /** + * Accept a deployed plugin to load in this host, or reject it, or adapt it for loading. + * The result may be a boolean to accept (`true`) or reject (`false`) the plugin as is, + * or else an adaptation of the original `plugin` to load in its stead. + */ + protected abstract acceptPlugin(plugin: DeployedPlugin): boolean | DeployedPlugin; + + /** + * Always synchronous in order to simplify handling disconnections. + * @throws never + */ + protected loadContributions(toDisconnect: DisposableCollection): Map { + let loaded = 0; + const loadPluginsMeasurement = this.measure('loadPlugins'); + + const hostContributions = new Map(); + console.log(`[${this.clientId}] Loading plugin contributions`); + for (const contributions of this.contributions.values()) { + const plugin = contributions.plugin.metadata; + const pluginId = plugin.model.id; + + if (contributions.state === PluginContributions.State.INITIALIZING) { + contributions.state = PluginContributions.State.LOADING; + contributions.push(Disposable.create(() => console.log(`[${pluginId}]: Unloaded plugin.`))); + contributions.push(this.handleContributions(contributions.plugin)); + contributions.state = PluginContributions.State.LOADED; + console.debug(`[${this.clientId}][${pluginId}]: Loaded contributions.`); + loaded++; + } + + if (contributions.state === PluginContributions.State.LOADED) { + contributions.state = PluginContributions.State.STARTING; + const host = plugin.model.entryPoint.frontend ? 'frontend' : plugin.host; + const dynamicContributions = hostContributions.get(host) || []; + dynamicContributions.push(contributions); + hostContributions.set(host, dynamicContributions); + toDisconnect.push(Disposable.create(() => { + contributions!.state = PluginContributions.State.LOADED; + console.debug(`[${this.clientId}][${pluginId}]: Disconnected.`); + })); + } + } + if (loaded > 0) { + // Only log load measurement if there are were plugins to load. + loadPluginsMeasurement?.log(`Load contributions of ${this.getPluginCount(loaded)}`); + } else { + loadPluginsMeasurement.stop(); + } + + return hostContributions; + } + + protected abstract handleContributions(plugin: DeployedPlugin): Disposable; + + protected async startPlugins(contributionsByHost: Map, toDisconnect: DisposableCollection): Promise { + let started = 0; + const startPluginsMeasurement = this.measure('startPlugins'); + + const [hostLogPath, hostStoragePath, hostGlobalStoragePath] = await Promise.all([ + this.pluginPathsService.getHostLogPath(), + this.getStoragePath(), + this.getHostGlobalStoragePath() + ]); + + if (toDisconnect.disposed) { + return; + } + + const thenable: Promise[] = []; + const configStorage: ConfigStorage = { + hostLogPath, + hostStoragePath, + hostGlobalStoragePath + }; + + for (const [host, hostContributions] of contributionsByHost) { + // do not start plugins for electron browser + if (host === 'frontend' && environment.electron.is()) { + continue; + } + + const manager = await this.obtainManager(host, hostContributions, toDisconnect); + if (!manager) { + continue; + } + + const plugins = hostContributions.map(contributions => contributions.plugin.metadata); + thenable.push((async () => { + try { + const activationEvents = [...this.activationEvents]; + await manager.$start({ plugins, configStorage, activationEvents }); + if (toDisconnect.disposed) { + return; + } + console.log(`[${this.clientId}] Starting plugins.`); + for (const contributions of hostContributions) { + started++; + const plugin = contributions.plugin; + const id = plugin.metadata.model.id; + contributions.state = PluginContributions.State.STARTED; + console.debug(`[${this.clientId}][${id}]: Started plugin.`); + toDisconnect.push(contributions.push(Disposable.create(() => { + console.debug(`[${this.clientId}][${id}]: Stopped plugin.`); + manager.$stop(id); + }))); + + this.handlePluginStarted(manager, plugin); + } + } catch (e) { + console.error(`Failed to start plugins for '${host}' host`, e); + } + })()); + } + + await Promise.all(thenable); + await this.activateByEvent('onStartupFinished'); + if (toDisconnect.disposed) { + return; + } + + if (started > 0) { + startPluginsMeasurement.log(`Start of ${this.getPluginCount(started)}`); + } else { + startPluginsMeasurement.stop(); + } + } + + protected abstract obtainManager(host: string, hostContributions: PluginContributions[], + toDisconnect: DisposableCollection): Promise; + + protected abstract getStoragePath(): Promise; + + protected abstract getHostGlobalStoragePath(): Promise; + + async activateByEvent(activationEvent: string): Promise { + if (this.activationEvents.has(activationEvent)) { + return; + } + this.activationEvents.add(activationEvent); + await Promise.all(Array.from(this.managers.values(), manager => manager.$activateByEvent(activationEvent))); + } + + async activatePlugin(id: string): Promise { + const activation = []; + for (const manager of this.managers.values()) { + activation.push(manager.$activatePlugin(id)); + } + await Promise.all(activation); + } + + protected handlePluginStarted(manager: PM, plugin: DeployedPlugin): void { + // Nothing to do in the abstract + } + + protected measure(name: string): Measurement { + return this.stopwatch.start(name, { context: this.clientId }); + } + + protected getPluginCount(plugins: number): string { + return `${plugins} plugin${plugins === 1 ? '' : 's'}`; + } + +} + +export class PluginContributions extends DisposableCollection { + constructor( + readonly plugin: DeployedPlugin + ) { + super(); + } + state: PluginContributions.State = PluginContributions.State.INITIALIZING; +} + +export namespace PluginContributions { + export enum State { + INITIALIZING = 0, + LOADING = 1, + LOADED = 2, + STARTING = 3, + STARTED = 4 + } +} diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts index a08e29c763e81..49cce6b693389 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts @@ -83,6 +83,12 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { return Array.from(this.deployedBackendPlugins.values()); } + async getDeployedPlugins(): Promise { + await this.frontendPluginsMetadataDeferred.promise; + await this.backendPluginsMetadataDeferred.promise; + return [...this.deployedFrontendPlugins.values(), ...this.deployedBackendPlugins.values()]; + } + getDeployedPluginsById(pluginId: string): DeployedPlugin[] { const matches: DeployedPlugin[] = []; const handle = (plugins: Iterable): void => { @@ -260,8 +266,8 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { const knownLocations = this.sourceLocations.get(id) ?? new Set(); const maybeStoredLocations = entry.getValue('sourceLocations'); const storedLocations = Array.isArray(maybeStoredLocations) && maybeStoredLocations.every(location => typeof location === 'string') - ? maybeStoredLocations.concat(entry.originalPath()) - : [entry.originalPath()]; + ? maybeStoredLocations.concat(entry.rootPath) + : [entry.rootPath]; storedLocations.forEach(location => knownLocations.add(location)); this.sourceLocations.set(id, knownLocations); } diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts index b3a3f660fbfcc..ec6560559a769 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts @@ -16,16 +16,16 @@ import { ConnectionErrorHandler, ContributionProvider, ILogger, MessageService } from '@theia/core/lib/common'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { BinaryMessagePipe } from '@theia/core/lib/node/messaging/binary-message-pipe'; import { createIpcEnv } from '@theia/core/lib/node/messaging/ipc-protocol'; import { inject, injectable, named } from '@theia/core/shared/inversify'; import * as cp from 'child_process'; +import { Duplex } from 'stream'; +import { DeployedPlugin, HostedPluginClient, PLUGIN_HOST_BACKEND, PluginHostEnvironmentVariable, PluginIdentifiers, ServerPluginRunner } from '../../common/plugin-protocol'; import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution'; import { HostedPluginLocalizationService } from './hosted-plugin-localization-service'; -import { ProcessTerminatedMessage, ProcessTerminateMessage } from './hosted-plugin-protocol'; -import { BinaryMessagePipe } from '@theia/core/lib/node/messaging/binary-message-pipe'; -import { DeployedPlugin, HostedPluginClient, PluginHostEnvironmentVariable, PluginIdentifiers, PLUGIN_HOST_BACKEND, ServerPluginRunner } from '../../common/plugin-protocol'; +import { ProcessTerminateMessage, ProcessTerminatedMessage } from './hosted-plugin-protocol'; import psTree = require('ps-tree'); -import { Duplex } from 'stream'; export interface IPCConnectionOptions { readonly serverName: string; @@ -149,13 +149,13 @@ export class HostedPluginProcess implements ServerPluginRunner { } } - public runPluginServer(): void { + public runPluginServer(serverName?: string): void { if (this.childProcess) { this.terminatePluginServer(); } this.terminatingPluginServer = false; this.childProcess = this.fork({ - serverName: 'hosted-plugin', + serverName: serverName ?? 'hosted-plugin', logger: this.logger, args: [] }); diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin.ts index 8728caa80e2f9..8e60f20f184f2 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin.ts @@ -85,9 +85,9 @@ export class HostedPluginSupport { } } - runPluginServer(): void { + runPluginServer(serverName?: string): void { if (!this.isPluginProcessRunning) { - this.hostedPluginProcess.runPluginServer(); + this.hostedPluginProcess.runPluginServer(serverName); this.isPluginProcessRunning = true; } } diff --git a/packages/plugin-ext/src/hosted/node/plugin-activation-events.ts b/packages/plugin-ext/src/hosted/node/plugin-activation-events.ts index a95b5138dd2d5..ddcec451da5a8 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-activation-events.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-activation-events.ts @@ -20,6 +20,7 @@ import { PluginPackage, PluginPackageAuthenticationProvider, PluginPackageCommand, + PluginPackageContribution, PluginPackageCustomEditor, PluginPackageLanguageContribution, PluginPackageNotebook, @@ -31,7 +32,7 @@ import { * This function will update the manifest based on the plugin contributions. */ export function updateActivationEvents(manifest: PluginPackage): void { - if (!isObject(manifest) || !isObject(manifest.contributes) || !manifest.contributes) { + if (!isObject(manifest) || !isObject(manifest.contributes) || !manifest.contributes) { return; } @@ -42,8 +43,8 @@ export function updateActivationEvents(manifest: PluginPackage): void { const commands = Array.isArray(value) ? value : [value]; updateCommandsContributions(commands, activationEvents); } - if (Array.isArray(manifest.contributes.views)) { - const views = flatten(Object.values(manifest.contributes.views)) as PluginPackageView[]; + if (isObject(manifest.contributes.views)) { + const views = flatten(Object.values(manifest.contributes.views)); updateViewsContribution(views, activationEvents); } if (Array.isArray(manifest.contributes.customEditors)) { @@ -64,7 +65,7 @@ export function updateActivationEvents(manifest: PluginPackage): void { function updateViewsContribution(views: PluginPackageView[], activationEvents: Set): void { for (const view of views) { - if (isObject(view) && typeof view.id === 'string') { + if (isObject(view) && typeof view.id === 'string') { activationEvents.add(`onView:${view.id}`); } } @@ -72,7 +73,7 @@ function updateViewsContribution(views: PluginPackageView[], activationEvents: S function updateCustomEditorsContribution(customEditors: PluginPackageCustomEditor[], activationEvents: Set): void { for (const customEditor of customEditors) { - if (isObject(customEditor) && typeof customEditor.viewType === 'string') { + if (isObject(customEditor) && typeof customEditor.viewType === 'string') { activationEvents.add(`onCustomEditor:${customEditor.viewType}`); } } @@ -80,7 +81,7 @@ function updateCustomEditorsContribution(customEditors: PluginPackageCustomEdito function updateCommandsContributions(commands: PluginPackageCommand[], activationEvents: Set): void { for (const command of commands) { - if (isObject(command) && typeof command.command === 'string') { + if (isObject(command) && typeof command.command === 'string') { activationEvents.add(`onCommand:${command.command}`); } } @@ -88,7 +89,7 @@ function updateCommandsContributions(commands: PluginPackageCommand[], activatio function updateAuthenticationProviderContributions(authProviders: PluginPackageAuthenticationProvider[], activationEvents: Set): void { for (const authProvider of authProviders) { - if (isObject(authProvider) && typeof authProvider.id === 'string') { + if (isObject(authProvider) && typeof authProvider.id === 'string') { activationEvents.add(`onAuthenticationRequest:${authProvider.id}`); } } @@ -96,7 +97,7 @@ function updateAuthenticationProviderContributions(authProviders: PluginPackageA function updateLanguageContributions(languages: PluginPackageLanguageContribution[], activationEvents: Set): void { for (const language of languages) { - if (isObject(language) && typeof language.id === 'string') { + if (isObject(language) && typeof language.id === 'string') { activationEvents.add(`onLanguage:${language.id}`); } } @@ -104,7 +105,7 @@ function updateLanguageContributions(languages: PluginPackageLanguageContributio function updateNotebookContributions(notebooks: PluginPackageNotebook[], activationEvents: Set): void { for (const notebook of notebooks) { - if (isObject(notebook) && typeof notebook.type === 'string') { + if (isObject(notebook) && typeof notebook.type === 'string') { activationEvents.add(`onNotebookSerializer:${notebook.type}`); } } diff --git a/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts b/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts index 78ce8fe2175e5..ae10070f58ea1 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts @@ -21,7 +21,7 @@ import { CliContribution } from '@theia/core/lib/node/cli'; import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { MetadataScanner } from './metadata-scanner'; -import { HostedPluginServerImpl } from './plugin-service'; +import { BackendPluginHostableFilter, HostedPluginServerImpl } from './plugin-service'; import { HostedPluginReader } from './plugin-reader'; import { HostedPluginSupport } from './hosted-plugin'; import { TheiaPluginScanner } from './scanners/scanner-theia'; @@ -38,6 +38,7 @@ import { LanguagePackService, languagePackServicePath } from '../../common/langu import { PluginLanguagePackService } from './plugin-language-pack-service'; import { RpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory'; import { ConnectionHandler } from '@theia/core/lib/common/messaging/handler'; +import { isConnectionScopedBackendPlugin } from '../common/hosted-plugin'; const commonHostedConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { bind(HostedPluginProcess).toSelf().inSingletonScope(); @@ -48,6 +49,7 @@ const commonHostedConnectionModule = ConnectionContainerModule.create(({ bind, b bind(HostedPluginServerImpl).toSelf().inSingletonScope(); bind(HostedPluginServer).toService(HostedPluginServerImpl); + bind(BackendPluginHostableFilter).toConstantValue(isConnectionScopedBackendPlugin); bindBackendService(hostedServicePath, HostedPluginServer, (server, client) => { server.setClient(client); client.onDidCloseConnection(() => server.dispose()); diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-module.ts b/packages/plugin-ext/src/hosted/node/plugin-host-module.ts new file mode 100644 index 0000000000000..94d22e0ddc474 --- /dev/null +++ b/packages/plugin-ext/src/hosted/node/plugin-host-module.ts @@ -0,0 +1,69 @@ +// ***************************************************************************** +// 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 '@theia/core/shared/reflect-metadata'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { RPCProtocol, RPCProtocolImpl } from '../../common/rpc-protocol'; +import { AbstractPluginHostRPC, PluginHostRPC, PluginContainerModuleLoader } from './plugin-host-rpc'; +import { AbstractPluginManagerExtImpl, MinimalTerminalServiceExt, PluginManagerExtImpl } from '../../plugin/plugin-manager'; +import { IPCChannel } from '@theia/core/lib/node'; +import { InternalPluginContainerModule } from '../../plugin/node/plugin-container-module'; +import { LocalizationExt } from '../../common/plugin-api-rpc'; +import { EnvExtImpl } from '../../plugin/env'; +import { EnvNodeExtImpl } from '../../plugin/node/env-node-ext'; +import { LocalizationExtImpl } from '../../plugin/localization-ext'; +import { PreferenceRegistryExtImpl } from '../../plugin/preference-registry'; +import { DebugExtImpl } from '../../plugin/debug/debug-ext'; +import { EditorsAndDocumentsExtImpl } from '../../plugin/editors-and-documents'; +import { WorkspaceExtImpl } from '../../plugin/workspace'; +import { MessageRegistryExt } from '../../plugin/message-registry'; +import { ClipboardExt } from '../../plugin/clipboard-ext'; +import { KeyValueStorageProxy, InternalStorageExt } from '../../plugin/plugin-storage'; +import { WebviewsExtImpl } from '../../plugin/webviews'; +import { TerminalServiceExtImpl } from '../../plugin/terminal-ext'; +import { InternalSecretsExt, SecretsExtImpl } from '../../plugin/secrets-ext'; + +export default new ContainerModule(bind => { + const channel = new IPCChannel(); + bind(RPCProtocol).toConstantValue(new RPCProtocolImpl(channel)); + + bind(PluginContainerModuleLoader).toDynamicValue(({ container }) => + (module: ContainerModule) => { + container.load(module); + const internalModule = module as InternalPluginContainerModule; + const pluginApiCache = internalModule.initializeApi?.(container); + return pluginApiCache; + }).inSingletonScope(); + + bind(AbstractPluginHostRPC).toService(PluginHostRPC); + bind(AbstractPluginManagerExtImpl).toService(PluginManagerExtImpl); + bind(PluginManagerExtImpl).toSelf().inSingletonScope(); + bind(PluginHostRPC).toSelf().inSingletonScope(); + bind(EnvExtImpl).to(EnvNodeExtImpl).inSingletonScope(); + bind(LocalizationExt).to(LocalizationExtImpl).inSingletonScope(); + bind(InternalStorageExt).toService(KeyValueStorageProxy); + bind(KeyValueStorageProxy).toSelf().inSingletonScope(); + bind(InternalSecretsExt).toService(SecretsExtImpl); + bind(SecretsExtImpl).toSelf().inSingletonScope(); + bind(PreferenceRegistryExtImpl).toSelf().inSingletonScope(); + bind(DebugExtImpl).toSelf().inSingletonScope(); + bind(EditorsAndDocumentsExtImpl).toSelf().inSingletonScope(); + bind(WorkspaceExtImpl).toSelf().inSingletonScope(); + bind(MessageRegistryExt).toSelf().inSingletonScope(); + bind(ClipboardExt).toSelf().inSingletonScope(); + bind(WebviewsExtImpl).toSelf().inSingletonScope(); + bind(MinimalTerminalServiceExt).toService(TerminalServiceExtImpl); + bind(TerminalServiceExtImpl).toSelf().inSingletonScope(); +}); diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts index eefeddaf5e7d0..05c6764a5c3bb 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts @@ -14,10 +14,15 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { dynamicRequire, removeFromCache } from '@theia/core/lib/node/dynamic-require'; -import { PluginManagerExtImpl } from '../../plugin/plugin-manager'; -import { LocalizationExt, MAIN_RPC_CONTEXT, Plugin, PluginAPIFactory } from '../../common/plugin-api-rpc'; -import { PluginMetadata } from '../../common/plugin-protocol'; +import { ContainerModule, inject, injectable, postConstruct, unmanaged } from '@theia/core/shared/inversify'; +import { AbstractPluginManagerExtImpl, PluginHost, PluginManagerExtImpl } from '../../plugin/plugin-manager'; +import { MAIN_RPC_CONTEXT, Plugin, PluginAPIFactory, PluginManager, + LocalizationExt +} from '../../common/plugin-api-rpc'; +import { PluginMetadata, PluginModel } from '../../common/plugin-protocol'; import { createAPIFactory } from '../../plugin/plugin-context'; import { EnvExtImpl } from '../../plugin/env'; import { PreferenceRegistryExtImpl } from '../../plugin/preference-registry'; @@ -26,95 +31,139 @@ import { DebugExtImpl } from '../../plugin/debug/debug-ext'; import { EditorsAndDocumentsExtImpl } from '../../plugin/editors-and-documents'; import { WorkspaceExtImpl } from '../../plugin/workspace'; import { MessageRegistryExt } from '../../plugin/message-registry'; -import { EnvNodeExtImpl } from '../../plugin/node/env-node-ext'; import { ClipboardExt } from '../../plugin/clipboard-ext'; import { loadManifest } from './plugin-manifest-loader'; import { KeyValueStorageProxy } from '../../plugin/plugin-storage'; import { WebviewsExtImpl } from '../../plugin/webviews'; import { TerminalServiceExtImpl } from '../../plugin/terminal-ext'; import { SecretsExtImpl } from '../../plugin/secrets-ext'; -import { BackendInitializationFn } from '../../common'; import { connectProxyResolver } from './plugin-host-proxy'; import { LocalizationExtImpl } from '../../plugin/localization-ext'; +import { RPCProtocol, ProxyIdentifier } from '../../common/rpc-protocol'; +import { PluginApiCache } from '../../plugin/node/plugin-container-module'; + +/** + * The full set of all possible `Ext` interfaces that a plugin manager can support. + */ +export interface ExtInterfaces { + envExt: EnvExtImpl, + storageExt: KeyValueStorageProxy, + debugExt: DebugExtImpl, + editorsAndDocumentsExt: EditorsAndDocumentsExtImpl, + messageRegistryExt: MessageRegistryExt, + workspaceExt: WorkspaceExtImpl, + preferenceRegistryExt: PreferenceRegistryExtImpl, + clipboardExt: ClipboardExt, + webviewExt: WebviewsExtImpl, + terminalServiceExt: TerminalServiceExtImpl, + secretsExt: SecretsExtImpl, + localizationExt: LocalizationExtImpl +} + +/** + * The RPC proxy identifier keys to set in the RPC object to register our `Ext` interface implementations. + */ +export type RpcKeys> = Partial>> & { + $pluginManager: ProxyIdentifier; +}; + +export const PluginContainerModuleLoader = Symbol('PluginContainerModuleLoader'); +/** + * A function that loads a `PluginContainerModule` exported by a plugin's entry-point + * script, returning the per-`Container` cache of its exported API instances if the + * module has an API factory registered. + */ +export type PluginContainerModuleLoader = (module: ContainerModule) => PluginApiCache | undefined; /** * Handle the RPC calls. + * + * @template PM is the plugin manager (ext) type + * @template PAF is the plugin API factory type + * @template EXT is the type identifying the `Ext` interfaces supported by the plugin manager */ -export class PluginHostRPC { +@injectable() +export abstract class AbstractPluginHostRPC, PAF, EXT extends Partial> { + + @inject(RPCProtocol) + protected readonly rpc: any; - private apiFactory: PluginAPIFactory; + @inject(PluginContainerModuleLoader) + protected readonly loadContainerModule: PluginContainerModuleLoader; - private pluginManager: PluginManagerExtImpl; + @inject(AbstractPluginManagerExtImpl) + protected readonly pluginManager: PM; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(protected readonly rpc: any) { + protected readonly banner: string; + + protected apiFactory: PAF; + + constructor( + @unmanaged() name: string, + @unmanaged() private readonly backendInitPath: string | undefined, + @unmanaged() private readonly extRpc: RpcKeys) { + this.banner = `${name}(${process.pid}):`; } + @postConstruct() initialize(): void { - const envExt = new EnvNodeExtImpl(this.rpc); - const storageProxy = new KeyValueStorageProxy(this.rpc); - const debugExt = new DebugExtImpl(this.rpc); - const editorsAndDocumentsExt = new EditorsAndDocumentsExtImpl(this.rpc); - const messageRegistryExt = new MessageRegistryExt(this.rpc); - const workspaceExt = new WorkspaceExtImpl(this.rpc, editorsAndDocumentsExt, messageRegistryExt); - const preferenceRegistryExt = new PreferenceRegistryExtImpl(this.rpc, workspaceExt); - const clipboardExt = new ClipboardExt(this.rpc); - const webviewExt = new WebviewsExtImpl(this.rpc, workspaceExt); - const terminalService = new TerminalServiceExtImpl(this.rpc); - const secretsExt = new SecretsExtImpl(this.rpc); - const localizationExt = new LocalizationExtImpl(this.rpc); - this.pluginManager = this.createPluginManager(envExt, terminalService, storageProxy, preferenceRegistryExt, webviewExt, secretsExt, localizationExt, this.rpc); - this.rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, this.pluginManager); - this.rpc.set(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT, editorsAndDocumentsExt); - this.rpc.set(MAIN_RPC_CONTEXT.WORKSPACE_EXT, workspaceExt); - this.rpc.set(MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT, preferenceRegistryExt); - this.rpc.set(MAIN_RPC_CONTEXT.STORAGE_EXT, storageProxy); - this.rpc.set(MAIN_RPC_CONTEXT.WEBVIEWS_EXT, webviewExt); - this.rpc.set(MAIN_RPC_CONTEXT.SECRETS_EXT, secretsExt); - - this.apiFactory = createAPIFactory( - this.rpc, - this.pluginManager, - envExt, - debugExt, - preferenceRegistryExt, - editorsAndDocumentsExt, - workspaceExt, - messageRegistryExt, - clipboardExt, - webviewExt, - localizationExt - ); - connectProxyResolver(workspaceExt, preferenceRegistryExt); + this.pluginManager.setPluginHost(this.createPluginHost()); + + const extInterfaces = this.createExtInterfaces(); + this.registerExtInterfaces(extInterfaces); + + this.apiFactory = this.createAPIFactory(extInterfaces); + + this.loadContainerModule(new ContainerModule(bind => bind(PluginManager).toConstantValue(this.pluginManager))); } async terminate(): Promise { await this.pluginManager.terminate(); } + protected abstract createAPIFactory(extInterfaces: EXT): PAF; + + protected abstract createExtInterfaces(): EXT; + + protected registerExtInterfaces(extInterfaces: EXT): void { + for (const _key in this.extRpc) { + if (Object.hasOwnProperty.call(this.extRpc, _key)) { + const key = _key as keyof ExtInterfaces; + // In case of present undefineds + if (extInterfaces[key]) { + this.rpc.set(this.extRpc[key], extInterfaces[key]); + } + } + } + this.rpc.set(this.extRpc.$pluginManager, this.pluginManager); + } + initContext(contextPath: string, plugin: Plugin): void { const { name, version } = plugin.rawModel; - console.debug('PLUGIN_HOST(' + process.pid + '): initializing(' + name + '@' + version + ' with ' + contextPath + ')'); + console.debug(this.banner, 'initializing(' + name + '@' + version + ' with ' + contextPath + ')'); try { - const backendInit = dynamicRequire<{ doInitialization: BackendInitializationFn }>(contextPath); + type BackendInitFn = (pluginApiFactory: PAF, plugin: Plugin) => void; + const backendInit = dynamicRequire<{ doInitialization: BackendInitFn }>(contextPath); backendInit.doInitialization(this.apiFactory, plugin); } catch (e) { console.error(e); } } - createPluginManager( - envExt: EnvExtImpl, terminalService: TerminalServiceExtImpl, storageProxy: KeyValueStorageProxy, - preferencesManager: PreferenceRegistryExtImpl, webview: WebviewsExtImpl, secretsExt: SecretsExtImpl, localization: LocalizationExt, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - rpc: any - ): PluginManagerExtImpl { + protected getBackendPluginPath(pluginModel: PluginModel): string | undefined { + return pluginModel.entryPoint.backend; + } + + /** + * Create the {@link PluginHost} that is required by my plugin manager ext interface to delegate + * critical behaviour such as loading and initializing plugins to me. + */ + createPluginHost(): PluginHost { const { extensionTestsPath } = process.env; const self = this; - const pluginManager = new PluginManagerExtImpl({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { loadPlugin(plugin: Plugin): any { - console.debug('PLUGIN_HOST(' + process.pid + '): PluginManagerExtImpl/loadPlugin(' + plugin.pluginPath + ')'); + console.debug(self.banner, 'PluginManagerExtImpl/loadPlugin(' + plugin.pluginPath + ')'); // cleaning the cache for all files of that plug-in. // this prevents a memory leak on plugin host restart. See for reference: // https://github.com/eclipse-theia/theia/pull/4931 @@ -125,7 +174,7 @@ export class PluginHostRPC { } }, async init(raw: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> { - console.log('PLUGIN_HOST(' + process.pid + '): PluginManagerExtImpl/init()'); + console.log(self.banner, 'PluginManagerExtImpl/init()'); const result: Plugin[] = []; const foreign: Plugin[] = []; for (const plg of raw) { @@ -146,14 +195,16 @@ export class PluginHostRPC { isUnderDevelopment: !!plg.isUnderDevelopment }); } else { + // Headless and backend plugins are, for now, very similar let backendInitPath = pluginLifecycle.backendInitPath; // if no init path, try to init as regular Theia plugin - if (!backendInitPath) { - backendInitPath = __dirname + '/scanners/backend-init-theia.js'; + if (!backendInitPath && self.backendInitPath) { + backendInitPath = __dirname + self.backendInitPath; } + const pluginPath = self.getBackendPluginPath(pluginModel); const plugin: Plugin = { - pluginPath: pluginModel.entryPoint.backend!, + pluginPath, pluginFolder: pluginModel.packagePath, pluginUri: pluginModel.packageUri, model: pluginModel, @@ -162,30 +213,30 @@ export class PluginHostRPC { isUnderDevelopment: !!plg.isUnderDevelopment }; - self.initContext(backendInitPath, plugin); - + if (backendInitPath) { + self.initContext(backendInitPath, plugin); + } else { + const { name, version } = plugin.rawModel; + console.debug(self.banner, 'initializing(' + name + '@' + version + ' without any default API)'); + } result.push(plugin); } } catch (e) { - console.error(`Failed to initialize ${plg.model.id} plugin.`, e); + console.error(self.banner, `Failed to initialize ${plg.model.id} plugin.`, e); } } return [result, foreign]; }, initExtApi(extApi: ExtPluginApi[]): void { for (const api of extApi) { - if (api.backendInitPath) { - try { - const extApiInit = dynamicRequire<{ provideApi: ExtPluginApiBackendInitializationFn }>(api.backendInitPath); - extApiInit.provideApi(rpc, pluginManager); - } catch (e) { - console.error(e); - } + try { + self.initExtApi(api); + } catch (e) { + console.error(e); } } }, loadTests: extensionTestsPath ? async () => { - /* eslint-disable @typescript-eslint/no-explicit-any */ // Require the test runner via node require from the provided path let testRunner: any; let requireError: Error | undefined; @@ -212,7 +263,115 @@ export class PluginHostRPC { `Path ${extensionTestsPath} does not point to a valid extension test runner.` ); } : undefined - }, envExt, terminalService, storageProxy, secretsExt, preferencesManager, webview, localization, rpc); - return pluginManager; + }; + } + + /** + * Initialize the end of the given provided extension API applicable to the current plugin host. + * Errors should be propagated to the caller. + * + * @param extApi the extension API to initialize, if appropriate + * @throws if any error occurs in initializing the extension API + */ + protected abstract initExtApi(extApi: ExtPluginApi): void; +} + +/** + * The RPC handler for frontend-connection-scoped plugins (Theia and VSCode plugins). + */ +@injectable() +export class PluginHostRPC extends AbstractPluginHostRPC { + @inject(EnvExtImpl) + protected readonly envExt: EnvExtImpl; + + @inject(LocalizationExt) + protected readonly localizationExt: LocalizationExtImpl; + + @inject(KeyValueStorageProxy) + protected readonly keyValueStorageProxy: KeyValueStorageProxy; + + @inject(DebugExtImpl) + protected readonly debugExt: DebugExtImpl; + + @inject(EditorsAndDocumentsExtImpl) + protected readonly editorsAndDocumentsExt: EditorsAndDocumentsExtImpl; + + @inject(MessageRegistryExt) + protected readonly messageRegistryExt: MessageRegistryExt; + + @inject(WorkspaceExtImpl) + protected readonly workspaceExt: WorkspaceExtImpl; + + @inject(PreferenceRegistryExtImpl) + protected readonly preferenceRegistryExt: PreferenceRegistryExtImpl; + + @inject(ClipboardExt) + protected readonly clipboardExt: ClipboardExt; + + @inject(WebviewsExtImpl) + protected readonly webviewExt: WebviewsExtImpl; + + @inject(TerminalServiceExtImpl) + protected readonly terminalServiceExt: TerminalServiceExtImpl; + + @inject(SecretsExtImpl) + protected readonly secretsExt: SecretsExtImpl; + + constructor() { + super('PLUGIN_HOST', '/scanners/backend-init-theia.js', + { + $pluginManager: MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, + editorsAndDocumentsExt: MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT, + workspaceExt: MAIN_RPC_CONTEXT.WORKSPACE_EXT, + preferenceRegistryExt: MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT, + storageExt: MAIN_RPC_CONTEXT.STORAGE_EXT, + webviewExt: MAIN_RPC_CONTEXT.WEBVIEWS_EXT, + secretsExt: MAIN_RPC_CONTEXT.SECRETS_EXT + } + ); + } + + protected createExtInterfaces(): ExtInterfaces { + connectProxyResolver(this.workspaceExt, this.preferenceRegistryExt); + return { + envExt: this.envExt, + storageExt: this.keyValueStorageProxy, + debugExt: this.debugExt, + editorsAndDocumentsExt: this.editorsAndDocumentsExt, + messageRegistryExt: this.messageRegistryExt, + workspaceExt: this.workspaceExt, + preferenceRegistryExt: this.preferenceRegistryExt, + clipboardExt: this.clipboardExt, + webviewExt: this.webviewExt, + terminalServiceExt: this.terminalServiceExt, + secretsExt: this.secretsExt, + localizationExt: this.localizationExt + }; + } + + protected createAPIFactory(extInterfaces: ExtInterfaces): PluginAPIFactory { + const { + envExt, debugExt, preferenceRegistryExt, editorsAndDocumentsExt, workspaceExt, + messageRegistryExt, clipboardExt, webviewExt, localizationExt + } = extInterfaces; + return createAPIFactory(this.rpc, this.pluginManager, envExt, debugExt, preferenceRegistryExt, + editorsAndDocumentsExt, workspaceExt, messageRegistryExt, clipboardExt, webviewExt, + localizationExt); + } + + protected initExtApi(extApi: ExtPluginApi): void { + interface PluginExports { + containerModule?: ContainerModule; + provideApi?: ExtPluginApiBackendInitializationFn; + } + if (extApi.backendInitPath) { + const { containerModule, provideApi } = dynamicRequire(extApi.backendInitPath); + if (containerModule) { + this.loadContainerModule(containerModule); + } + if (provideApi) { + provideApi(this.rpc, this.pluginManager); + } + } } } diff --git a/packages/plugin-ext/src/hosted/node/plugin-host.ts b/packages/plugin-ext/src/hosted/node/plugin-host.ts index 0159a153e52fe..cee2ad5b75d5b 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host.ts @@ -14,10 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import '@theia/core/shared/reflect-metadata'; -import { ConnectionClosedError, RPCProtocolImpl } from '../../common/rpc-protocol'; +import { Container } from '@theia/core/shared/inversify'; +import { ConnectionClosedError, RPCProtocol } from '../../common/rpc-protocol'; import { ProcessTerminatedMessage, ProcessTerminateMessage } from './hosted-plugin-protocol'; import { PluginHostRPC } from './plugin-host-rpc'; -import { IPCChannel } from '@theia/core/lib/node'; +import pluginHostModule from './plugin-host-module'; console.log('PLUGIN_HOST(' + process.pid + ') starting instance'); @@ -74,8 +75,12 @@ process.on('rejectionHandled', (promise: Promise) => { }); let terminating = false; -const channel = new IPCChannel(); -const rpc = new RPCProtocolImpl(channel); + +const container = new Container(); +container.load(pluginHostModule); + +const rpc: RPCProtocol = container.get(RPCProtocol); +const pluginHostRPC = container.get(PluginHostRPC); process.on('message', async (message: string) => { if (terminating) { @@ -103,6 +108,3 @@ process.on('message', async (message: string) => { console.error(e); } }); - -const pluginHostRPC = new PluginHostRPC(rpc); -pluginHostRPC.initialize(); diff --git a/packages/plugin-ext/src/hosted/node/plugin-reader.ts b/packages/plugin-ext/src/hosted/node/plugin-reader.ts index b137673a3acfd..f90df35151083 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-reader.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-reader.ts @@ -108,6 +108,9 @@ export class HostedPluginReader implements BackendApplicationContribution { if (pluginMetadata.model.entryPoint.backend) { pluginMetadata.model.entryPoint.backend = path.resolve(plugin.packagePath, pluginMetadata.model.entryPoint.backend); } + if (pluginMetadata.model.entryPoint.headless) { + pluginMetadata.model.entryPoint.headless = path.resolve(plugin.packagePath, pluginMetadata.model.entryPoint.headless); + } if (pluginMetadata) { // Add post processor if (this.metadataProcessors) { diff --git a/packages/plugin-ext/src/hosted/node/plugin-service.ts b/packages/plugin-ext/src/hosted/node/plugin-service.ts index 1c67db3e5109d..9b1d09a899ae7 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-service.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-service.ts @@ -13,7 +13,7 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject, named, postConstruct } from '@theia/core/shared/inversify'; +import { injectable, inject, named, optional, postConstruct } from '@theia/core/shared/inversify'; import { HostedPluginServer, HostedPluginClient, PluginDeployer, GetDeployedPluginsParams, DeployedPlugin, PluginIdentifiers } from '../../common/plugin-protocol'; import { HostedPluginSupport } from './hosted-plugin'; import { ILogger, Disposable, ContributionProvider, DisposableCollection } from '@theia/core'; @@ -23,6 +23,14 @@ import { PluginDeployerImpl } from '../../main/node/plugin-deployer-impl'; import { HostedPluginLocalizationService } from './hosted-plugin-localization-service'; import { PluginUninstallationManager } from '../../main/node/plugin-uninstallation-manager'; +export const BackendPluginHostableFilter = Symbol('BackendPluginHostableFilter'); +/** + * A filter matching backend plugins that are hostable in my plugin host process. + * Only if at least one backend plugin is deployed that matches my filter will I + * start the host process. + */ +export type BackendPluginHostableFilter = (plugin: DeployedPlugin) => boolean; + @injectable() export class HostedPluginServerImpl implements HostedPluginServer { @inject(ILogger) @@ -43,10 +51,15 @@ export class HostedPluginServerImpl implements HostedPluginServer { @inject(PluginUninstallationManager) protected readonly uninstallationManager: PluginUninstallationManager; + @inject(BackendPluginHostableFilter) + @optional() + protected backendPluginHostableFilter: BackendPluginHostableFilter; + protected client: HostedPluginClient | undefined; protected toDispose = new DisposableCollection(); protected _ignoredPlugins?: Set; + // We ignore any plugins that are marked as uninstalled the first time the frontend requests information about deployed plugins. protected get ignoredPlugins(): Set { if (!this._ignoredPlugins) { @@ -63,6 +76,10 @@ export class HostedPluginServerImpl implements HostedPluginServer { @postConstruct() protected init(): void { + if (!this.backendPluginHostableFilter) { + this.backendPluginHostableFilter = () => true; + } + this.toDispose.pushAll([ this.pluginDeployer.onDidDeploy(() => this.client?.onDidDeploy()), this.uninstallationManager.onDidChangeUninstalledPlugins(currentUninstalled => { @@ -80,6 +97,10 @@ export class HostedPluginServerImpl implements HostedPluginServer { ]); } + protected getServerName(): string { + return 'hosted-plugin'; + } + dispose(): void { this.toDispose.dispose(); } @@ -90,9 +111,10 @@ export class HostedPluginServerImpl implements HostedPluginServer { } async getDeployedPluginIds(): Promise { - const backendMetadata = await this.deployerHandler.getDeployedBackendPluginIds(); - if (backendMetadata.length > 0) { - this.hostedPlugin.runPluginServer(); + const backendPlugins = (await this.deployerHandler.getDeployedBackendPlugins()) + .filter(this.backendPluginHostableFilter); + if (backendPlugins.length > 0) { + this.hostedPlugin.runPluginServer(this.getServerName()); } const plugins = new Set(); const addIds = async (identifiers: PluginIdentifiers.VersionedId[]): Promise => { @@ -103,7 +125,7 @@ export class HostedPluginServerImpl implements HostedPluginServer { } }; addIds(await this.deployerHandler.getDeployedFrontendPluginIds()); - addIds(backendMetadata); + addIds(await this.deployerHandler.getDeployedBackendPluginIds()); addIds(await this.hostedPlugin.getExtraDeployedPluginIds()); return Array.from(plugins); } diff --git a/packages/plugin-ext/src/hosted/node/scanners/file-plugin-uri-factory.ts b/packages/plugin-ext/src/hosted/node/scanners/file-plugin-uri-factory.ts index 3ad460efd6c78..74518f5097d66 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/file-plugin-uri-factory.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/file-plugin-uri-factory.ts @@ -17,7 +17,7 @@ import { injectable } from '@theia/core/shared/inversify'; import * as path from 'path'; import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { PluginPackage } from '../../../common'; import { PluginUriFactory } from './plugin-uri-factory'; /** diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 6f05afe4f8d1c..338f5eea7368e 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -62,7 +62,9 @@ import { Translation, PluginIdentifiers, TerminalProfile, - PluginIconContribution + PluginIconContribution, + PluginEntryPoint, + PluginPackageContribution } from '../../../common/plugin-protocol'; import { promises as fs } from 'fs'; import * as path from 'path'; @@ -87,17 +89,19 @@ function getFileExtension(filePath: string): string { return index === -1 ? '' : filePath.substring(index + 1); } -@injectable() -export class TheiaPluginScanner implements PluginScanner { +type PluginPackageWithContributes = PluginPackage & { contributes: PluginPackageContribution }; - private readonly _apiType: PluginEngine = 'theiaPlugin'; +@injectable() +export abstract class AbstractPluginScanner implements PluginScanner { @inject(GrammarsReader) - private readonly grammarsReader: GrammarsReader; + protected readonly grammarsReader: GrammarsReader; @inject(PluginUriFactory) protected readonly pluginUriFactory: PluginUriFactory; + constructor(private readonly _apiType: PluginEngine, private readonly _backendInitPath?: string) { } + get apiType(): PluginEngine { return this._apiType; } @@ -119,22 +123,25 @@ export class TheiaPluginScanner implements PluginScanner { type: this._apiType, version: plugin.engines[this._apiType] }, - entryPoint: { - frontend: plugin.theiaPlugin!.frontend, - backend: plugin.theiaPlugin!.backend - } + entryPoint: this.getEntryPoint(plugin) }; return result; } + protected abstract getEntryPoint(plugin: PluginPackage): PluginEntryPoint; + getLifecycle(plugin: PluginPackage): PluginLifecycle { - return { + const result: PluginLifecycle = { startMethod: 'start', stopMethod: 'stop', frontendModuleName: buildFrontendModuleName(plugin), - - backendInitPath: path.join(__dirname, 'backend-init-theia') }; + + if (this._backendInitPath) { + result.backendInitPath = path.join(__dirname, this._backendInitPath); + } + + return result; } getDependencies(rawPlugin: PluginPackage): Map | undefined { @@ -155,13 +162,52 @@ export class TheiaPluginScanner implements PluginScanner { return contributions; } + return this.readContributions(rawPlugin as PluginPackageWithContributes, contributions); + } + + protected async readContributions(rawPlugin: PluginPackageWithContributes, contributions: PluginContribution): Promise { + return contributions; + } + +} + +@injectable() +export class TheiaPluginScanner extends AbstractPluginScanner { + constructor() { + super('theiaPlugin', 'backend-init-theia'); + } + + protected getEntryPoint(plugin: PluginPackage): PluginEntryPoint { + const result: PluginEntryPoint = { + frontend: plugin.theiaPlugin!.frontend, + backend: plugin.theiaPlugin!.backend + }; + if (plugin.theiaPlugin?.headless) { + result.headless = plugin.theiaPlugin.headless; + } + return result; + } + + protected override async readContributions(rawPlugin: PluginPackageWithContributes, contributions: PluginContribution): Promise { try { if (rawPlugin.contributes.configuration) { const configurations = Array.isArray(rawPlugin.contributes.configuration) ? rawPlugin.contributes.configuration : [rawPlugin.contributes.configuration]; + const hasMultipleConfigs = configurations.length > 1; contributions.configuration = []; for (const c of configurations) { const config = this.readConfiguration(c, rawPlugin.packagePath); if (config) { + Object.values(config.properties).forEach(property => { + if (hasMultipleConfigs) { + // If there are multiple configuration contributions, we need to distinguish them by their title in the settings UI. + // They are placed directly under the plugin's name in the settings UI. + property.owner = rawPlugin.displayName; + property.group = config.title; + } else { + // If there's only one configuration contribution, we display the title in the settings UI. + property.owner = config.title; + } + }); contributions.configuration.push(config); } } @@ -307,13 +353,19 @@ export class TheiaPluginScanner implements PluginScanner { try { contributions.notebooks = rawPlugin.contributes.notebooks; } catch (err) { - console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.authentication, err); + console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.notebooks, err); } try { contributions.notebookRenderer = rawPlugin.contributes.notebookRenderer; } catch (err) { - console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.authentication, err); + console.error(`Could not read '${rawPlugin.name}' contribution 'notebook-renderer'.`, rawPlugin.contributes.notebookRenderer, err); + } + + try { + contributions.notebookPreload = rawPlugin.contributes.notebookPreload; + } catch (err) { + console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks-preload'.`, rawPlugin.contributes.notebookPreload, err); } try { @@ -415,9 +467,9 @@ export class TheiaPluginScanner implements PluginScanner { return translation; } - protected readCommand({ command, title, original, category, icon, enablement }: PluginPackageCommand, pck: PluginPackage): PluginCommand { + protected readCommand({ command, title, shortTitle, original, category, icon, enablement }: PluginPackageCommand, pck: PluginPackage): PluginCommand { const { themeIcon, iconUrl } = this.transformIconUrl(pck, icon) ?? {}; - return { command, title, originalTitle: original, category, iconUrl, themeIcon, enablement }; + return { command, title, shortTitle, originalTitle: original, category, iconUrl, themeIcon, enablement }; } protected transformIconUrl(plugin: PluginPackage, original?: IconUrl): { iconUrl?: IconUrl; themeIcon?: string } | undefined { diff --git a/packages/plugin-ext/src/main/browser/authentication-main.ts b/packages/plugin-ext/src/main/browser/authentication-main.ts index 046e5a7c81751..58f3c4d8c935d 100644 --- a/packages/plugin-ext/src/main/browser/authentication-main.ts +++ b/packages/plugin-ext/src/main/browser/authentication-main.ts @@ -27,7 +27,10 @@ import { MessageService } from '@theia/core/lib/common/message-service'; import { ConfirmDialog, Dialog, StorageService } from '@theia/core/lib/browser'; import { AuthenticationProvider, + AuthenticationProviderSessionOptions, AuthenticationService, + AuthenticationSession, + AuthenticationSessionAccountInformation, readAllowedExtensions } from '@theia/core/lib/browser/authentication-service'; import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; @@ -77,9 +80,13 @@ export class AuthenticationMainImpl implements AuthenticationMain { return this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); } + $getAccounts(providerId: string): Thenable { + return this.authenticationService.getSessions(providerId).then(sessions => sessions.map(session => session.account)); + } + async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: theia.AuthenticationGetSessionOptions): Promise { - const sessions = await this.authenticationService.getSessions(providerId, scopes); + const sessions = await this.authenticationService.getSessions(providerId, scopes, options?.account); // Error cases if (options.forceNewSession && !sessions.length) { @@ -140,26 +147,32 @@ export class AuthenticationMainImpl implements AuthenticationMain { } protected async selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, - potentialSessions: Readonly, scopes: string[], clearSessionPreference: boolean): Promise { + potentialSessions: Readonly, scopes: string[], clearSessionPreference: boolean): Promise { + if (!potentialSessions.length) { throw new Error('No potential sessions found'); } return new Promise(async (resolve, reject) => { - const items: QuickPickValue<{ session?: theia.AuthenticationSession }>[] = potentialSessions.map(session => ({ + const items: QuickPickValue<{ session?: AuthenticationSession, account?: AuthenticationSessionAccountInformation }>[] = potentialSessions.map(session => ({ label: session.account.label, value: { session } })); items.push({ label: nls.localizeByDefault('Sign in to another account'), - value: { session: undefined } + value: {} }); + + // VS Code has code here that pushes accounts that have no active sessions. However, since we do not store + // any accounts that don't have sessions, we dont' do this. const selected = await this.quickPickService.show(items, { title: nls.localizeByDefault("The extension '{0}' wants to access a {1} account", extensionName, providerName), ignoreFocusOut: true }); if (selected) { + + // if we ever have accounts without sessions, pass the account to the login call const session = selected.value?.session ?? await this.authenticationService.login(providerId, scopes); const accountName = session.account.label; @@ -318,13 +331,13 @@ export class AuthenticationProviderImpl implements AuthenticationProvider { } } - async getSessions(scopes?: string[]): Promise> { - return this.proxy.$getSessions(this.id, scopes); + async getSessions(scopes?: string[], account?: AuthenticationSessionAccountInformation): Promise> { + return this.proxy.$getSessions(this.id, scopes, { account: account }); } async updateSessionItems(event: theia.AuthenticationProviderAuthenticationSessionsChangeEvent): Promise { const { added, removed } = event; - const session = await this.proxy.$getSessions(this.id); + const session = await this.proxy.$getSessions(this.id, undefined, {}); const addedSessions = added ? session.filter(s => added.some(addedSession => addedSession.id === s.id)) : []; removed?.forEach(removedSession => { @@ -347,21 +360,23 @@ export class AuthenticationProviderImpl implements AuthenticationProvider { addedSessions.forEach(s => this.registerSession(s)); } - async login(scopes: string[]): Promise { - return this.createSession(scopes); + async login(scopes: string[], options: AuthenticationProviderSessionOptions): Promise { + return this.createSession(scopes, options); } async logout(sessionId: string): Promise { return this.removeSession(sessionId); } - createSession(scopes: string[]): Thenable { - return this.proxy.$createSession(this.id, scopes); + createSession(scopes: string[], options: AuthenticationProviderSessionOptions): Thenable { + return this.proxy.$createSession(this.id, scopes, options); } removeSession(sessionId: string): Thenable { return this.proxy.$removeSession(this.id, sessionId) - .then(() => { this.messageService.info('Successfully signed out.'); }); + .then(() => { + this.messageService.info(nls.localizeByDefault('Successfully signed out.')); + }); } } diff --git a/packages/plugin-ext/src/main/browser/command-registry-main.ts b/packages/plugin-ext/src/main/browser/command-registry-main.ts index 70f614c85dabd..6ce49b91b3c7d 100644 --- a/packages/plugin-ext/src/main/browser/command-registry-main.ts +++ b/packages/plugin-ext/src/main/browser/command-registry-main.ts @@ -23,6 +23,9 @@ import { RPCProtocol } from '../../common/rpc-protocol'; import { KeybindingRegistry } from '@theia/core/lib/browser'; import { PluginContributionHandler } from './plugin-contribution-handler'; import { ArgumentProcessor } from '../../common/commands'; +import { ContributionProvider } from '@theia/core'; + +export const ArgumentProcessorContribution = Symbol('ArgumentProcessorContribution'); export class CommandRegistryMainImpl implements CommandRegistryMain, Disposable { private readonly proxy: CommandRegistryExt; @@ -41,6 +44,10 @@ export class CommandRegistryMainImpl implements CommandRegistryMain, Disposable this.delegate = container.get(CommandRegistry); this.keyBinding = container.get(KeybindingRegistry); this.contributions = container.get(PluginContributionHandler); + + container.getNamed>(ContributionProvider, ArgumentProcessorContribution).getContributions().forEach(processor => { + this.registerArgumentProcessor(processor); + }); } dispose(): void { diff --git a/packages/plugin-ext/src/main/browser/comments/comments-contribution.ts b/packages/plugin-ext/src/main/browser/comments/comments-contribution.ts index a26c302bcf97a..ee144563b105d 100644 --- a/packages/plugin-ext/src/main/browser/comments/comments-contribution.ts +++ b/packages/plugin-ext/src/main/browser/comments/comments-contribution.ts @@ -26,6 +26,7 @@ import { CommandRegistry, DisposableCollection, MenuModelRegistry } from '@theia import { URI } from '@theia/core/shared/vscode-uri'; import { CommentsContextKeyService } from './comments-context-key-service'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { Uri } from '@theia/plugin'; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. @@ -64,14 +65,16 @@ export class CommentsContribution { if (editor instanceof MonacoDiffEditor) { const originalEditorModel = editor.diffEditor.getOriginalEditor().getModel(); if (originalEditorModel) { - const originalComments = await this.commentService.getComments(originalEditorModel.uri); + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + const originalComments = await this.commentService.getComments(originalEditorModel.uri as Uri); if (originalComments) { this.rangeDecorator.update(editor.diffEditor.getOriginalEditor(), originalComments.filter(c => !!c)); } } const modifiedEditorModel = editor.diffEditor.getModifiedEditor().getModel(); if (modifiedEditorModel) { - const modifiedComments = await this.commentService.getComments(modifiedEditorModel.uri); + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + const modifiedComments = await this.commentService.getComments(modifiedEditorModel.uri as Uri); if (modifiedComments) { this.rangeDecorator.update(editor.diffEditor.getModifiedEditor(), modifiedComments.filter(c => !!c)); } @@ -164,7 +167,8 @@ export class CommentsContribution { const editorModel = this.editor && this.editor.getModel(); const editorURI = this.editor && editorModel && editorModel.uri; if (editorURI) { - const comments = await this.commentService.getComments(editorURI); + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + const comments = await this.commentService.getComments(editorURI as Uri); this.setComments(comments.filter(c => !!c)); } } diff --git a/packages/plugin-ext/src/main/browser/comments/comments-main.ts b/packages/plugin-ext/src/main/browser/comments/comments-main.ts index 4aab5f5d45914..c4e6051be9764 100644 --- a/packages/plugin-ext/src/main/browser/comments/comments-main.ts +++ b/packages/plugin-ext/src/main/browser/comments/comments-main.ts @@ -38,7 +38,7 @@ import { URI } from '@theia/core/shared/vscode-uri'; import { CancellationToken } from '@theia/core/lib/common'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { interfaces } from '@theia/core/shared/inversify'; -import { v4 as uuidv4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import { CommentsContribution } from './comments-contribution'; /*--------------------------------------------------------------------------------------------- @@ -392,7 +392,7 @@ export class CommentsMainImp implements CommentsMain { } $registerCommentController(handle: number, id: string, label: string): void { - const providerId = uuidv4(); + const providerId = generateUuid(); this.handlers.set(handle, providerId); const provider = new CommentController(this.proxy, this.commentService, handle, providerId, id, label, {}); diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-contribution.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-contribution.ts deleted file mode 100644 index 485cdaa697eb4..0000000000000 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-contribution.ts +++ /dev/null @@ -1,38 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2021 SAP SE or an SAP affiliate company 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 { injectable, inject } from '@theia/core/shared/inversify'; -import { CommandRegistry, CommandContribution } from '@theia/core/lib/common'; -import { ApplicationShell, CommonCommands } from '@theia/core/lib/browser'; -import { CustomEditorWidget } from './custom-editor-widget'; - -@injectable() -export class CustomEditorContribution implements CommandContribution { - - @inject(ApplicationShell) - protected readonly shell: ApplicationShell; - - registerCommands(commands: CommandRegistry): void { - commands.registerHandler(CommonCommands.UNDO.id, { - isEnabled: () => this.shell.activeWidget instanceof CustomEditorWidget, - execute: () => (this.shell.activeWidget as CustomEditorWidget).undo() - }); - commands.registerHandler(CommonCommands.REDO.id, { - isEnabled: () => this.shell.activeWidget instanceof CustomEditorWidget, - execute: () => (this.shell.activeWidget as CustomEditorWidget).redo() - }); - } -} diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-opener.tsx b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-opener.tsx index 245ab57cb1e29..4c9746a9f5914 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-opener.tsx +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-opener.tsx @@ -14,13 +14,15 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; -import { ApplicationShell, OpenHandler, Widget, WidgetManager, WidgetOpenerOptions } from '@theia/core/lib/browser'; +import { + ApplicationShell, DiffUris, OpenHandler, OpenerOptions, PreferenceService, SplitWidget, Widget, WidgetManager, WidgetOpenerOptions, getDefaultHandler, defaultHandlerPriority +} from '@theia/core/lib/browser'; import { CustomEditor, CustomEditorPriority, CustomEditorSelector } from '../../../common'; import { CustomEditorWidget } from './custom-editor-widget'; -import { v4 } from 'uuid'; -import { Emitter } from '@theia/core'; +import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry'; +import { generateUuid } from '@theia/core/lib/common/uuid'; +import { DisposableCollection, Emitter } from '@theia/core'; import { match } from '@theia/core/lib/common/glob'; export class CustomEditorOpener implements OpenHandler { @@ -33,8 +35,10 @@ export class CustomEditorOpener implements OpenHandler { constructor( private readonly editor: CustomEditor, - @inject(ApplicationShell) protected readonly shell: ApplicationShell, - @inject(WidgetManager) protected readonly widgetManager: WidgetManager + protected readonly shell: ApplicationShell, + protected readonly widgetManager: WidgetManager, + protected readonly editorRegistry: PluginCustomEditorRegistry, + protected readonly preferenceService: PreferenceService ) { this.id = CustomEditorOpener.toCustomEditorId(this.editor.viewType); this.label = this.editor.displayName; @@ -44,7 +48,25 @@ export class CustomEditorOpener implements OpenHandler { return `custom-editor-${editorViewType}`; } - canHandle(uri: URI): number { + canHandle(uri: URI, options?: OpenerOptions): number { + let priority = 0; + const { selector } = this.editor; + if (DiffUris.isDiffUri(uri)) { + const [left, right] = DiffUris.decode(uri); + if (this.matches(selector, right) && this.matches(selector, left)) { + priority = this.getPriority(); + } + } else if (this.matches(selector, uri)) { + if (getDefaultHandler(uri, this.preferenceService) === this.editor.viewType) { + priority = defaultHandlerPriority; + } else { + priority = this.getPriority(); + } + } + return priority; + } + + canOpenWith(uri: URI): number { if (this.matches(this.editor.selector, uri)) { return this.getPriority(); } @@ -62,34 +84,112 @@ export class CustomEditorOpener implements OpenHandler { } protected readonly pendingWidgetPromises = new Map>(); - async open(uri: URI, options?: WidgetOpenerOptions): Promise { + protected async openCustomEditor(uri: URI, options?: WidgetOpenerOptions): Promise { let widget: CustomEditorWidget | undefined; - const widgets = this.widgetManager.getWidgets(CustomEditorWidget.FACTORY_ID) as CustomEditorWidget[]; - widget = widgets.find(w => w.viewType === this.editor.viewType && w.resource.toString() === uri.toString()); - - if (widget?.isVisible) { - return this.shell.revealWidget(widget.id); - } - if (widget?.isAttached) { - return this.shell.activateWidget(widget.id); - } - if (!widget) { - const uriString = uri.toString(); - let widgetPromise = this.pendingWidgetPromises.get(uriString); - if (!widgetPromise) { - const id = v4(); - widgetPromise = this.widgetManager.getOrCreateWidget(CustomEditorWidget.FACTORY_ID, { id }); + let isNewWidget = false; + const uriString = uri.toString(); + let widgetPromise = this.pendingWidgetPromises.get(uriString); + if (widgetPromise) { + widget = await widgetPromise; + } else { + const widgets = this.widgetManager.getWidgets(CustomEditorWidget.FACTORY_ID) as CustomEditorWidget[]; + widget = widgets.find(w => w.viewType === this.editor.viewType && w.resource.toString() === uriString); + if (!widget) { + isNewWidget = true; + const id = generateUuid(); + widgetPromise = this.widgetManager.getOrCreateWidget(CustomEditorWidget.FACTORY_ID, { id }).then(async w => { + try { + w.viewType = this.editor.viewType; + w.resource = uri; + await this.editorRegistry.resolveWidget(w); + if (options?.widgetOptions) { + await this.shell.addWidget(w, options.widgetOptions); + } + return w; + } catch (e) { + w.dispose(); + throw e; + } + }).finally(() => this.pendingWidgetPromises.delete(uriString)); this.pendingWidgetPromises.set(uriString, widgetPromise); widget = await widgetPromise; - this.pendingWidgetPromises.delete(uriString); - widget.viewType = this.editor.viewType; - widget.resource = uri; - this.onDidOpenCustomEditorEmitter.fire([widget, options]); } } + if (options?.mode === 'activate') { + await this.shell.activateWidget(widget.id); + } else if (options?.mode === 'reveal') { + await this.shell.revealWidget(widget.id); + } + if (isNewWidget) { + this.onDidOpenCustomEditorEmitter.fire([widget, options]); + } return widget; } + protected async openSideBySide(uri: URI, options?: WidgetOpenerOptions): Promise { + const [leftUri, rightUri] = DiffUris.decode(uri); + const widget = await this.widgetManager.getOrCreateWidget( + CustomEditorWidget.SIDE_BY_SIDE_FACTORY_ID, { uri: uri.toString(), viewType: this.editor.viewType }); + if (!widget.panes.length) { // a new widget + const trackedDisposables = new DisposableCollection(widget); + try { + const createPane = async (paneUri: URI) => { + let pane = await this.openCustomEditor(paneUri); + if (pane.isAttached) { + await this.shell.closeWidget(pane.id); + if (!pane.isDisposed) { // user canceled + return undefined; + } + pane = await this.openCustomEditor(paneUri); + } + return pane; + }; + + const rightPane = await createPane(rightUri); + if (!rightPane) { + trackedDisposables.dispose(); + return undefined; + } + trackedDisposables.push(rightPane); + + const leftPane = await createPane(leftUri); + if (!leftPane) { + trackedDisposables.dispose(); + return undefined; + } + trackedDisposables.push(leftPane); + + widget.addPane(leftPane); + widget.addPane(rightPane); + + // dispose the widget if either of its panes gets externally disposed + leftPane.disposed.connect(() => widget.dispose()); + rightPane.disposed.connect(() => widget.dispose()); + + if (options?.widgetOptions) { + await this.shell.addWidget(widget, options.widgetOptions); + } + } catch (e) { + trackedDisposables.dispose(); + console.error(e); + throw e; + } + } + if (options?.mode === 'activate') { + await this.shell.activateWidget(widget.id); + } else if (options?.mode === 'reveal') { + await this.shell.revealWidget(widget.id); + } + return widget; + } + + async open(uri: URI, options?: WidgetOpenerOptions): Promise { + options = { ...options }; + options.mode ??= 'activate'; + options.widgetOptions ??= { area: 'main' }; + return DiffUris.isDiffUri(uri) ? this.openSideBySide(uri, options) : this.openCustomEditor(uri, options); + } + matches(selectors: CustomEditorSelector[], resource: URI): boolean { return selectors.some(selector => this.selectorMatches(selector, resource)); } diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-undo-redo-handler.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-undo-redo-handler.ts new file mode 100644 index 0000000000000..328433ad36aec --- /dev/null +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-undo-redo-handler.ts @@ -0,0 +1,41 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { ApplicationShell, UndoRedoHandler } from '@theia/core/lib/browser'; +import { CustomEditorWidget } from './custom-editor-widget'; + +@injectable() +export class CustomEditorUndoRedoHandler implements UndoRedoHandler { + + @inject(ApplicationShell) + protected readonly applicationShell: ApplicationShell; + + priority = 190; + select(): CustomEditorWidget | undefined { + const current = this.applicationShell.currentWidget; + if (current instanceof CustomEditorWidget) { + return current; + } + return undefined; + } + undo(item: CustomEditorWidget): void { + item.undo(); + } + redo(item: CustomEditorWidget): void { + item.redo(); + } +} diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts index cee4ee0fc9416..ac804199fc89b 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts @@ -17,45 +17,43 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { FileOperation } from '@theia/filesystem/lib/common/files'; -import { ApplicationShell, NavigatableWidget, Saveable, SaveableSource, SaveOptions } from '@theia/core/lib/browser'; -import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service'; +import { ApplicationShell, DelegatingSaveable, NavigatableWidget, Saveable, SaveableSource, SaveOptions } from '@theia/core/lib/browser'; +import { SaveableService } from '@theia/core/lib/browser/saveable-service'; import { Reference } from '@theia/core/lib/common/reference'; import { WebviewWidget } from '../webview/webview'; -import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; import { CustomEditorModel } from './custom-editors-main'; +import { CustomEditorWidget as CustomEditorWidgetShape } from '@theia/editor/lib/browser'; @injectable() -export class CustomEditorWidget extends WebviewWidget implements SaveableSource, NavigatableWidget { +export class CustomEditorWidget extends WebviewWidget implements CustomEditorWidgetShape, SaveableSource, NavigatableWidget { static override FACTORY_ID = 'plugin-custom-editor'; + static readonly SIDE_BY_SIDE_FACTORY_ID = CustomEditorWidget.FACTORY_ID + '.side-by-side'; override id: string; resource: URI; - protected _modelRef: Reference; - get modelRef(): Reference { + protected _modelRef: Reference = { object: undefined, dispose: () => { } }; + get modelRef(): Reference { return this._modelRef; } set modelRef(modelRef: Reference) { + this._modelRef.dispose(); this._modelRef = modelRef; + this.delegatingSaveable.delegate = modelRef.object; this.doUpdateContent(); - Saveable.apply( - this, - () => this.shell.widgets.filter(widget => !!Saveable.get(widget)), - (widget, options) => this.saveService.save(widget, options), - ); } + + // ensures that saveable is available even if modelRef.object is undefined + protected readonly delegatingSaveable = new DelegatingSaveable(); get saveable(): Saveable { - return this._modelRef.object; + return this.delegatingSaveable; } - @inject(UndoRedoService) - protected readonly undoRedoService: UndoRedoService; - @inject(ApplicationShell) protected readonly shell: ApplicationShell; - @inject(SaveResourceService) - protected readonly saveService: SaveResourceService; + @inject(SaveableService) + protected readonly saveService: SaveableService; @postConstruct() protected override init(): void { @@ -69,21 +67,23 @@ export class CustomEditorWidget extends WebviewWidget implements SaveableSource, } undo(): void { - this.undoRedoService.undo(this.resource); + this._modelRef.object?.undo(); } redo(): void { - this.undoRedoService.redo(this.resource); + this._modelRef.object?.redo(); } async save(options?: SaveOptions): Promise { - await this._modelRef.object.saveCustomEditor(options); + await this._modelRef.object?.saveCustomEditor(options); } async saveAs(source: URI, target: URI, options?: SaveOptions): Promise { - const result = await this._modelRef.object.saveCustomEditorAs(source, target, options); - this.doMove(target); - return result; + if (this._modelRef.object) { + const result = await this._modelRef.object.saveCustomEditorAs(source, target, options); + this.doMove(target); + return result; + } } getResourceUri(): URI | undefined { diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts index ba9a5850654ed..ef5c2d7d60bf4 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts @@ -24,8 +24,7 @@ import { MAIN_RPC_CONTEXT, CustomEditorsMain, CustomEditorsExt, CustomTextEditor import { RPCProtocol } from '../../../common/rpc-protocol'; import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin'; import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry'; -import { CustomEditorWidget } from './custom-editor-widget'; -import { Emitter, UNTITLED_SCHEME } from '@theia/core'; +import { Emitter } from '@theia/core'; import { UriComponents } from '../../../common/uri-components'; import { URI } from '@theia/core/shared/vscode-uri'; import TheiaURI from '@theia/core/lib/common/uri'; @@ -39,11 +38,9 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; import { WebviewsMainImpl } from '../webviews-main'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; -import { ApplicationShell, DefaultUriLabelProviderContribution, Saveable, SaveOptions, WidgetOpenerOptions } from '@theia/core/lib/browser'; -import { WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions } from '@theia/plugin'; -import { WebviewWidgetIdentifier } from '../webview/webview'; +import { ApplicationShell, LabelProvider, Saveable, SaveOptions } from '@theia/core/lib/browser'; +import { WebviewPanelOptions } from '@theia/plugin'; import { EditorPreferences } from '@theia/editor/lib/browser'; -import { ViewColumn, WebviewPanelTargetArea } from '../../../plugin/types-impl'; const enum CustomEditorModelType { Custom, @@ -58,7 +55,7 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { protected readonly customEditorService: CustomEditorService; protected readonly undoRedoService: UndoRedoService; protected readonly customEditorRegistry: PluginCustomEditorRegistry; - protected readonly labelProvider: DefaultUriLabelProviderContribution; + protected readonly labelProvider: LabelProvider; protected readonly widgetManager: WidgetManager; protected readonly editorPreferences: EditorPreferences; private readonly proxy: CustomEditorsExt; @@ -75,7 +72,7 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { this.customEditorService = container.get(CustomEditorService); this.undoRedoService = container.get(UndoRedoService); this.customEditorRegistry = container.get(PluginCustomEditorRegistry); - this.labelProvider = container.get(DefaultUriLabelProviderContribution); + this.labelProvider = container.get(LabelProvider); this.editorPreferences = container.get(EditorPreferences); this.widgetManager = container.get(WidgetManager); this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT); @@ -111,7 +108,8 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { const disposables = new DisposableCollection(); disposables.push( - this.customEditorRegistry.registerResolver(viewType, async (widget, widgetOpenerOptions) => { + this.customEditorRegistry.registerResolver(viewType, async widget => { + const { resource, identifier } = widget; widget.options = options; @@ -139,18 +137,21 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { widget.onMove(async (newResource: TheiaURI) => { const oldModel = modelRef; modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, onMoveCancelTokenSource.token); - this.proxy.$onMoveCustomEditor(identifier.id, URI.file(newResource.path.toString()), viewType); + this.proxy.$onMoveCustomEditor(identifier.id, newResource.toComponents(), viewType); oldModel.dispose(); }); } + this.webviewsMain.hookWebview(widget); + widget.title.label = this.labelProvider.getName(resource); + const _cancellationSource = new CancellationTokenSource(); await this.proxy.$resolveWebviewEditor( - URI.file(resource.path.toString()), + resource.toComponents(), identifier.id, viewType, - this.labelProvider.getName(resource)!, - widgetOpenerOptions, + widget.title.label, + widget.viewState.position, options, _cancellationSource.token ); @@ -189,7 +190,7 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { return this.customEditorService.models.add(resource, viewType, model); } case CustomEditorModelType.Custom: { - const model = MainCustomEditorModel.create(this.proxy, viewType, resource, this.undoRedoService, this.fileService, this.editorPreferences, cancellationToken); + const model = MainCustomEditorModel.create(this.proxy, viewType, resource, this.undoRedoService, this.fileService, cancellationToken); return this.customEditorService.models.add(resource, viewType, model); } } @@ -213,66 +214,6 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { const model = await this.getCustomEditorModel(resourceComponents, viewType); model.changeContent(); } - - async $createCustomEditorPanel( - panelId: string, - title: string, - widgetOpenerOptions: WidgetOpenerOptions | undefined, - options: WebviewPanelOptions & WebviewOptions - ): Promise { - const view = await this.widgetManager.getOrCreateWidget(CustomEditorWidget.FACTORY_ID, { id: panelId }); - this.webviewsMain.hookWebview(view); - view.title.label = title; - const { enableFindWidget, retainContextWhenHidden, enableScripts, enableForms, localResourceRoots, ...contentOptions } = options; - view.viewColumn = ViewColumn.One; // behaviour might be overridden later using widgetOpenerOptions (if available) - view.options = { enableFindWidget, retainContextWhenHidden }; - view.setContentOptions({ - allowScripts: enableScripts, - allowForms: enableForms, - localResourceRoots: localResourceRoots && localResourceRoots.map(root => root.toString()), - ...contentOptions, - ...view.contentOptions - }); - if (view.isAttached) { - if (view.isVisible) { - this.shell.revealWidget(view.id); - } - return; - } - const showOptions: WebviewPanelShowOptions = { - preserveFocus: true - }; - - if (widgetOpenerOptions) { - if (widgetOpenerOptions.mode === 'reveal') { - showOptions.preserveFocus = false; - } - - if (widgetOpenerOptions.widgetOptions) { - let area: WebviewPanelTargetArea; - switch (widgetOpenerOptions.widgetOptions.area) { - case 'main': - area = WebviewPanelTargetArea.Main; - case 'left': - area = WebviewPanelTargetArea.Left; - case 'right': - area = WebviewPanelTargetArea.Right; - case 'bottom': - area = WebviewPanelTargetArea.Bottom; - default: // includes 'top' and 'secondaryWindow' - area = WebviewPanelTargetArea.Main; - } - showOptions.area = area; - - if (widgetOpenerOptions.widgetOptions.mode === 'split-right' || - widgetOpenerOptions.widgetOptions.mode === 'open-to-right') { - showOptions.viewColumn = ViewColumn.Beside; - } - } - } - - this.webviewsMain.addOrReattachWidget(view, showOptions); - } } export interface CustomEditorModel extends Saveable, Disposable { @@ -284,6 +225,9 @@ export interface CustomEditorModel extends Saveable, Disposable { revert(options?: Saveable.RevertOptions): Promise; saveCustomEditor(options?: SaveOptions): Promise; saveCustomEditorAs(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise; + + undo(): void; + redo(): void; } export class MainCustomEditorModel implements CustomEditorModel { @@ -297,8 +241,8 @@ export class MainCustomEditorModel implements CustomEditorModel { private readonly onDirtyChangedEmitter = new Emitter(); readonly onDirtyChanged = this.onDirtyChangedEmitter.event; - autoSave: 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange'; - autoSaveDelay: number; + private readonly onContentChangedEmitter = new Emitter(); + readonly onContentChanged = this.onContentChangedEmitter.event; static async create( proxy: CustomEditorsExt, @@ -306,11 +250,10 @@ export class MainCustomEditorModel implements CustomEditorModel { resource: TheiaURI, undoRedoService: UndoRedoService, fileService: FileService, - editorPreferences: EditorPreferences, cancellation: CancellationToken, ): Promise { - const { editable } = await proxy.$createCustomDocument(URI.file(resource.path.toString()), viewType, {}, cancellation); - return new MainCustomEditorModel(proxy, viewType, resource, editable, undoRedoService, fileService, editorPreferences); + const { editable } = await proxy.$createCustomDocument(resource.toComponents(), viewType, {}, cancellation); + return new MainCustomEditorModel(proxy, viewType, resource, editable, undoRedoService, fileService); } constructor( @@ -319,27 +262,13 @@ export class MainCustomEditorModel implements CustomEditorModel { private readonly editorResource: TheiaURI, private readonly editable: boolean, private readonly undoRedoService: UndoRedoService, - private readonly fileService: FileService, - private readonly editorPreferences: EditorPreferences + private readonly fileService: FileService ) { - this.autoSave = this.editorPreferences.get('files.autoSave', undefined, editorResource.toString()); - this.autoSaveDelay = this.editorPreferences.get('files.autoSaveDelay', undefined, editorResource.toString()); - - this.toDispose.push( - this.editorPreferences.onPreferenceChanged(event => { - if (event.preferenceName === 'files.autoSave') { - this.autoSave = this.editorPreferences.get('files.autoSave', undefined, editorResource.toString()); - } - if (event.preferenceName === 'files.autoSaveDelay') { - this.autoSaveDelay = this.editorPreferences.get('files.autoSaveDelay', undefined, editorResource.toString()); - } - }) - ); this.toDispose.push(this.onDirtyChangedEmitter); } get resource(): URI { - return URI.file(this.editorResource.path.toString()); + return URI.from(this.editorResource.toComponents()); } get dirty(): boolean { @@ -441,7 +370,7 @@ export class MainCustomEditorModel implements CustomEditorModel { async saveCustomEditorAs(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise { if (this.editable) { const source = new CancellationTokenSource(); - await this.proxy.$onSaveAs(this.resource, this.viewType, URI.file(targetResource.path.toString()), source.token); + await this.proxy.$onSaveAs(this.resource, this.viewType, targetResource.toComponents(), source.token); this.change(() => { this.savePoint = this.currentEditIndex; }); @@ -451,7 +380,7 @@ export class MainCustomEditorModel implements CustomEditorModel { } } - private async undo(): Promise { + async undo(): Promise { if (!this.editable) { return; } @@ -468,7 +397,7 @@ export class MainCustomEditorModel implements CustomEditorModel { await this.proxy.$undo(this.resource, this.viewType, undoneEdit, this.dirty); } - private async redo(): Promise { + async redo(): Promise { if (!this.editable) { return; } @@ -505,13 +434,7 @@ export class MainCustomEditorModel implements CustomEditorModel { if (this.dirty !== wasDirty) { this.onDirtyChangedEmitter.fire(); } - - if (this.autoSave !== 'off' && this.dirty && this.resource.scheme !== UNTITLED_SCHEME) { - const handle = window.setTimeout(() => { - this.save(); - window.clearTimeout(handle); - }, this.autoSaveDelay); - } + this.onContentChangedEmitter.fire(); } } @@ -521,6 +444,8 @@ export class CustomTextEditorModel implements CustomEditorModel { private readonly toDispose = new DisposableCollection(); private readonly onDirtyChangedEmitter = new Emitter(); readonly onDirtyChanged = this.onDirtyChangedEmitter.event; + private readonly onContentChangedEmitter = new Emitter(); + readonly onContentChanged = this.onContentChangedEmitter.event; static async create( viewType: string, @@ -544,15 +469,13 @@ export class CustomTextEditorModel implements CustomEditorModel { this.onDirtyChangedEmitter.fire(); }) ); + this.toDispose.push( + this.editorTextModel.onContentChanged(e => { + this.onContentChangedEmitter.fire(); + }) + ); this.toDispose.push(this.onDirtyChangedEmitter); - } - - get autoSave(): 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange' { - return this.editorTextModel.autoSave; - } - - get autoSaveDelay(): number { - return this.editorTextModel.autoSaveDelay; + this.toDispose.push(this.onContentChangedEmitter); } dispose(): void { @@ -561,7 +484,7 @@ export class CustomTextEditorModel implements CustomEditorModel { } get resource(): URI { - return URI.file(this.editorResource.path.toString()); + return URI.from(this.editorResource.toComponents()); } get dirty(): boolean { @@ -569,7 +492,7 @@ export class CustomTextEditorModel implements CustomEditorModel { }; get readonly(): boolean { - return this.editorTextModel.readOnly; + return Boolean(this.editorTextModel.readOnly); } get editorTextModel(): MonacoEditorModel { @@ -592,4 +515,12 @@ export class CustomTextEditorModel implements CustomEditorModel { await this.saveCustomEditor(options); await this.fileService.copy(resource, targetResource, { overwrite: false }); } + + undo(): void { + this.editorTextModel.undo(); + } + + redo(): void { + this.editorTextModel.redo(); + } } diff --git a/packages/plugin-ext/src/main/browser/custom-editors/plugin-custom-editor-registry.ts b/packages/plugin-ext/src/main/browser/custom-editors/plugin-custom-editor-registry.ts index fbb58b10f0479..27738141688cf 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/plugin-custom-editor-registry.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/plugin-custom-editor-registry.ts @@ -15,22 +15,19 @@ // ***************************************************************************** import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; -import { CustomEditor } from '../../../common'; +import { CustomEditor, DeployedPlugin } from '../../../common'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Deferred } from '@theia/core/lib/common/promise-util'; import { CustomEditorOpener } from './custom-editor-opener'; -import { WorkspaceCommands } from '@theia/workspace/lib/browser'; -import { CommandRegistry, Emitter, MenuModelRegistry } from '@theia/core'; -import { SelectionService } from '@theia/core/lib/common'; -import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; -import { NavigatorContextMenu } from '@theia/navigator/lib//browser/navigator-contribution'; -import { ApplicationShell, DefaultOpenerService, WidgetManager, WidgetOpenerOptions } from '@theia/core/lib/browser'; +import { Emitter } from '@theia/core'; +import { ApplicationShell, DefaultOpenerService, OpenWithService, PreferenceService, WidgetManager } from '@theia/core/lib/browser'; import { CustomEditorWidget } from './custom-editor-widget'; @injectable() export class PluginCustomEditorRegistry { private readonly editors = new Map(); - private readonly pendingEditors = new Set(); - private readonly resolvers = new Map void>(); + private readonly pendingEditors = new Map, disposable: Disposable }>(); + private readonly resolvers = new Map Promise>(); private readonly onWillOpenCustomEditorEmitter = new Emitter(); readonly onWillOpenCustomEditor = this.onWillOpenCustomEditorEmitter.event; @@ -38,21 +35,18 @@ export class PluginCustomEditorRegistry { @inject(DefaultOpenerService) protected readonly defaultOpenerService: DefaultOpenerService; - @inject(MenuModelRegistry) - protected readonly menuModelRegistry: MenuModelRegistry; - - @inject(CommandRegistry) - protected readonly commandRegistry: CommandRegistry; - - @inject(SelectionService) - protected readonly selectionService: SelectionService; - @inject(WidgetManager) protected readonly widgetManager: WidgetManager; @inject(ApplicationShell) protected readonly shell: ApplicationShell; + @inject(OpenWithService) + protected readonly openWithService: OpenWithService; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + @postConstruct() protected init(): void { this.widgetManager.onDidCreateWidget(({ factoryId, widget }) => { @@ -71,7 +65,7 @@ export class PluginCustomEditorRegistry { }); } - registerCustomEditor(editor: CustomEditor): Disposable { + registerCustomEditor(editor: CustomEditor, plugin: DeployedPlugin): Disposable { if (this.editors.has(editor.viewType)) { console.warn('editor with such id already registered: ', JSON.stringify(editor)); return Disposable.NULL; @@ -84,54 +78,44 @@ export class PluginCustomEditorRegistry { const editorOpenHandler = new CustomEditorOpener( editor, this.shell, - this.widgetManager + this.widgetManager, + this, + this.preferenceService ); toDispose.push(this.defaultOpenerService.addHandler(editorOpenHandler)); - - const openWithCommand = WorkspaceCommands.FILE_OPEN_WITH(editorOpenHandler); - toDispose.push( - this.menuModelRegistry.registerMenuAction( - NavigatorContextMenu.OPEN_WITH, - { - commandId: openWithCommand.id, - label: editorOpenHandler.label - } - ) - ); - toDispose.push( - this.commandRegistry.registerCommand( - openWithCommand, - UriAwareCommandHandler.MonoSelect(this.selectionService, { - execute: uri => editorOpenHandler.open(uri), - isEnabled: uri => editorOpenHandler.canHandle(uri) > 0, - isVisible: uri => editorOpenHandler.canHandle(uri) > 0 - }) - ) - ); toDispose.push( - editorOpenHandler.onDidOpenCustomEditor(event => this.resolveWidget(event[0], event[1])) + this.openWithService.registerHandler({ + id: editor.viewType, + label: editorOpenHandler.label, + providerName: plugin.metadata.model.displayName, + canHandle: uri => editorOpenHandler.canOpenWith(uri), + open: uri => editorOpenHandler.open(uri) + }) ); return toDispose; } - resolveWidget = (widget: CustomEditorWidget, options?: WidgetOpenerOptions) => { + async resolveWidget(widget: CustomEditorWidget): Promise { const resolver = this.resolvers.get(widget.viewType); if (resolver) { - resolver(widget, options); + await resolver(widget); } else { - this.pendingEditors.add(widget); + const deferred = new Deferred(); + const disposable = widget.onDidDispose(() => this.pendingEditors.delete(widget)); + this.pendingEditors.set(widget, { deferred, disposable }); this.onWillOpenCustomEditorEmitter.fire(widget.viewType); + return deferred.promise; } }; - registerResolver(viewType: string, resolver: (widget: CustomEditorWidget, options?: WidgetOpenerOptions) => void): Disposable { + registerResolver(viewType: string, resolver: (widget: CustomEditorWidget) => Promise): Disposable { if (this.resolvers.has(viewType)) { throw new Error(`Resolver for ${viewType} already registered`); } - for (const editorWidget of this.pendingEditors) { + for (const [editorWidget, { deferred, disposable }] of this.pendingEditors.entries()) { if (editorWidget.viewType === viewType) { - resolver(editorWidget); + resolver(editorWidget).then(() => deferred.resolve(), err => deferred.reject(err)).finally(() => disposable.dispose()); this.pendingEditors.delete(editorWidget); } } diff --git a/packages/plugin-ext/src/main/browser/data-transfer/data-transfer-type-converters.ts b/packages/plugin-ext/src/main/browser/data-transfer/data-transfer-type-converters.ts index b3c5c175ed473..c2e281e31f816 100644 --- a/packages/plugin-ext/src/main/browser/data-transfer/data-transfer-type-converters.ts +++ b/packages/plugin-ext/src/main/browser/data-transfer/data-transfer-type-converters.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { IDataTransferItem, VSDataTransfer } from '@theia/monaco-editor-core/esm/vs/base/common/dataTransfer'; +import { IDataTransferItem, IReadonlyVSDataTransfer } from '@theia/monaco-editor-core/esm/vs/base/common/dataTransfer'; import { DataTransferDTO, DataTransferItemDTO } from '../../../common/plugin-api-rpc-model'; import { URI } from '../../../plugin/types-impl'; @@ -24,7 +24,6 @@ export namespace DataTransferItem { if (mime === 'text/uri-list') { return { - id: item.id, asString: '', fileData: undefined, uriListData: serializeUriList(stringValue), @@ -33,9 +32,8 @@ export namespace DataTransferItem { const fileValue = item.asFile(); return { - id: item.id, asString: stringValue, - fileData: fileValue ? { name: fileValue.name, uri: fileValue.uri } : undefined, + fileData: fileValue ? { id: fileValue.id, name: fileValue.name, uri: fileValue.uri } : undefined, }; } @@ -57,10 +55,10 @@ export namespace DataTransferItem { } export namespace DataTransfer { - export async function toDataTransferDTO(value: VSDataTransfer): Promise { + export async function toDataTransferDTO(value: IReadonlyVSDataTransfer): Promise { return { items: await Promise.all( - Array.from(value.entries()) + Array.from(value) .map( async ([mime, item]) => [mime, await DataTransferItem.from(mime, item)] ) diff --git a/packages/plugin-ext/src/main/browser/debug/debug-main.ts b/packages/plugin-ext/src/main/browser/debug/debug-main.ts index 8e1020a8a7fb1..471310a0b44a6 100644 --- a/packages/plugin-ext/src/main/browser/debug/debug-main.ts +++ b/packages/plugin-ext/src/main/browser/debug/debug-main.ts @@ -25,7 +25,7 @@ import { MAIN_RPC_CONTEXT } from '../../../common/plugin-api-rpc'; import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; -import { Breakpoint, WorkspaceFolder } from '../../../common/plugin-api-rpc-model'; +import { Breakpoint, DebugStackFrameDTO, DebugThreadDTO, WorkspaceFolder } from '../../../common/plugin-api-rpc-model'; import { LabelProvider } from '@theia/core/lib/browser'; import { EditorManager } from '@theia/editor/lib/browser'; import { BreakpointManager, BreakpointsChangeEvent } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager'; @@ -56,6 +56,9 @@ import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution'; import { ConnectionImpl } from '../../../common/connection'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { DebugSessionOptions as TheiaDebugSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; +import { DebugStackFrame } from '@theia/debug/lib/browser/model/debug-stack-frame'; +import { DebugThread } from '@theia/debug/lib/browser/model/debug-thread'; +import { TestService } from '@theia/test/lib/browser/test-service'; export class DebugMainImpl implements DebugMain, Disposable { private readonly debugExt: DebugExt; @@ -75,6 +78,7 @@ export class DebugMainImpl implements DebugMain, Disposable { private readonly fileService: FileService; private readonly pluginService: HostedPluginSupport; private readonly debugContributionProvider: ContributionProvider; + private readonly testService: TestService; private readonly workspaceService: WorkspaceService; private readonly debuggerContributions = new Map(); @@ -98,6 +102,7 @@ export class DebugMainImpl implements DebugMain, Disposable { this.debugContributionProvider = container.getNamed(ContributionProvider, DebugContribution); this.fileService = container.get(FileService); this.pluginService = container.get(HostedPluginSupport); + this.testService = container.get(TestService); this.workspaceService = container.get(WorkspaceService); const fireDidChangeBreakpoints = ({ added, removed, changed }: BreakpointsChangeEvent) => { @@ -117,7 +122,9 @@ export class DebugMainImpl implements DebugMain, Disposable { this.sessionManager.onDidStartDebugSession(debugSession => this.debugExt.$sessionDidStart(debugSession.id)), this.sessionManager.onDidDestroyDebugSession(debugSession => this.debugExt.$sessionDidDestroy(debugSession.id)), this.sessionManager.onDidChangeActiveDebugSession(event => this.debugExt.$sessionDidChange(event.current && event.current.id)), - this.sessionManager.onDidReceiveDebugSessionCustomEvent(event => this.debugExt.$onSessionCustomEvent(event.session.id, event.event, event.body)) + this.sessionManager.onDidReceiveDebugSessionCustomEvent(event => this.debugExt.$onSessionCustomEvent(event.session.id, event.event, event.body)), + this.sessionManager.onDidFocusStackFrame(stackFrame => this.debugExt.$onDidChangeActiveFrame(this.toDebugStackFrameDTO(stackFrame))), + this.sessionManager.onDidFocusThread(debugThread => this.debugExt.$onDidChangeActiveThread(this.toDebugThreadDTO(debugThread))), ]); } @@ -161,6 +168,7 @@ export class DebugMainImpl implements DebugMain, Disposable { this.fileService, terminalOptionsExt, this.debugContributionProvider, + this.testService, this.workspaceService, ); @@ -323,6 +331,7 @@ export class DebugMainImpl implements DebugMain, Disposable { } else { sessionOptions = { ...sessionOptions, ...options, workspaceFolderUri }; } + sessionOptions.testRun = options.testRun; // start options const session = await this.sessionManager.start(sessionOptions); @@ -340,6 +349,21 @@ export class DebugMainImpl implements DebugMain, Disposable { } } + private toDebugStackFrameDTO(stackFrame: DebugStackFrame | undefined): DebugStackFrameDTO | undefined { + return stackFrame ? { + sessionId: stackFrame.session.id, + frameId: stackFrame.frameId, + threadId: stackFrame.thread.threadId + } : undefined; + } + + private toDebugThreadDTO(debugThread: DebugThread | undefined): DebugThreadDTO | undefined { + return debugThread ? { + sessionId: debugThread.session.id, + threadId: debugThread.threadId + } : undefined; + } + private toTheiaPluginApiBreakpoints(breakpoints: (SourceBreakpoint | FunctionBreakpoint)[]): Breakpoint[] { return breakpoints.map(b => this.toTheiaPluginApiBreakpoint(b)); } diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts index 6d64adad17f0f..fb612bb3ea6ab 100644 --- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts @@ -97,6 +97,13 @@ export class PluginDebugService implements DebugService { }, 100); registerDebugConfigurationProvider(provider: PluginDebugConfigurationProvider): Disposable { + if (this.configurationProviders.has(provider.handle)) { + const configuration = this.configurationProviders.get(provider.handle); + if (configuration && configuration.type !== provider.type) { + console.warn(`Different debug configuration provider with type '${configuration.type}' already registered.`); + provider.handle = this.configurationProviders.size; + } + } const handle = provider.handle; this.configurationProviders.set(handle, provider); this.fireOnDidConfigurationProvidersChanged(); diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts index 02f88e0dc9b71..2cd49a625ec7b 100644 --- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts @@ -22,7 +22,7 @@ import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { MessageClient } from '@theia/core/lib/common/message-service-protocol'; import { OutputChannelManager } from '@theia/output/lib/browser/output-channel'; import { DebugPreferences } from '@theia/debug/lib/browser/debug-preferences'; -import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; +import { DebugConfigurationSessionOptions, TestRunReference } from '@theia/debug/lib/browser/debug-session-options'; import { DebugSession } from '@theia/debug/lib/browser/debug-session'; import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection'; import { TerminalWidgetOptions, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; @@ -32,12 +32,17 @@ import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { PluginChannel } from '../../../common/connection'; +import { TestService } from '@theia/test/lib/browser/test-service'; +import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; export class PluginDebugSession extends DebugSession { constructor( override readonly id: string, override readonly options: DebugConfigurationSessionOptions, override readonly parentSession: DebugSession | undefined, + testService: TestService, + testRun: TestRunReference | undefined, + sessionManager: DebugSessionManager, protected override readonly connection: DebugSessionConnection, protected override readonly terminalServer: TerminalService, protected override readonly editorManager: EditorManager, @@ -48,7 +53,8 @@ export class PluginDebugSession extends DebugSession { protected readonly terminalOptionsExt: TerminalOptionsExt | undefined, protected override readonly debugContributionProvider: ContributionProvider, protected override readonly workspaceService: WorkspaceService) { - super(id, options, parentSession, connection, terminalServer, editorManager, breakpoints, labelProvider, messages, fileService, debugContributionProvider, + super(id, options, parentSession, testService, testRun, sessionManager, connection, terminalServer, editorManager, breakpoints, + labelProvider, messages, fileService, debugContributionProvider, workspaceService); } @@ -75,12 +81,13 @@ export class PluginDebugSessionFactory extends DefaultDebugSessionFactory { protected override readonly fileService: FileService, protected readonly terminalOptionsExt: TerminalOptionsExt | undefined, protected override readonly debugContributionProvider: ContributionProvider, + protected override readonly testService: TestService, protected override readonly workspaceService: WorkspaceService, ) { super(); } - override get(sessionId: string, options: DebugConfigurationSessionOptions, parentSession?: DebugSession): DebugSession { + override get(manager: DebugSessionManager, sessionId: string, options: DebugConfigurationSessionOptions, parentSession?: DebugSession): DebugSession { const connection = new DebugSessionConnection( sessionId, this.connectionFactory, @@ -90,6 +97,9 @@ export class PluginDebugSessionFactory extends DefaultDebugSessionFactory { sessionId, options, parentSession, + this.testService, + options.testRun, + manager, connection, this.terminalService, this.editorManager, diff --git a/packages/plugin-ext/src/main/browser/documents-main.ts b/packages/plugin-ext/src/main/browser/documents-main.ts index 798738759d998..b9f4b64782b14 100644 --- a/packages/plugin-ext/src/main/browser/documents-main.ts +++ b/packages/plugin-ext/src/main/browser/documents-main.ts @@ -20,10 +20,10 @@ import { DisposableCollection, Disposable, UntitledResourceResolver } from '@the import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; import { RPCProtocol } from '../../common/rpc-protocol'; import { EditorModelService } from './text-editor-model-service'; -import { EditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser'; +import { EditorOpenerOptions } from '@theia/editor/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { URI as CodeURI } from '@theia/core/shared/vscode-uri'; -import { ApplicationShell, Saveable } from '@theia/core/lib/browser'; +import { ApplicationShell } from '@theia/core/lib/browser'; import { TextDocumentShowOptions } from '../../common/plugin-api-rpc-model'; import { Range } from '@theia/core/shared/vscode-languageserver-protocol'; import { OpenerService } from '@theia/core/lib/browser/opener-service'; @@ -32,6 +32,7 @@ import { dispose } from '../../common/disposable-util'; import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages'; import * as monaco from '@theia/monaco-editor-core'; import { TextDocumentChangeReason } from '../../plugin/types-impl'; +import { NotebookDocumentsMainImpl } from './notebooks/notebook-documents-main'; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. @@ -90,9 +91,9 @@ export class DocumentsMainImpl implements DocumentsMain, Disposable { constructor( editorsAndDocuments: EditorsAndDocumentsMain, + notebookDocuments: NotebookDocumentsMainImpl, private readonly modelService: EditorModelService, rpc: RPCProtocol, - private editorManager: EditorManager, private openerService: OpenerService, private shell: ApplicationShell, private untitledResourceResolver: UntitledResourceResolver, @@ -105,6 +106,8 @@ export class DocumentsMainImpl implements DocumentsMain, Disposable { this.toDispose.push(editorsAndDocuments.onDocumentRemove(documents => documents.forEach(this.onModelRemoved, this))); this.toDispose.push(modelService.onModelModeChanged(this.onModelChanged, this)); + this.toDispose.push(notebookDocuments.onDidAddNotebookCellModel(this.onModelAdded, this)); + this.toDispose.push(modelService.onModelSaved(m => { this.proxy.$acceptModelSaved(m.textEditorModel.uri); })); @@ -202,13 +205,7 @@ export class DocumentsMainImpl implements DocumentsMain, Disposable { } async $trySaveDocument(uri: UriComponents): Promise { - const widget = await this.editorManager.getByUri(new URI(CodeURI.revive(uri))); - if (widget) { - await Saveable.save(widget); - return true; - } - - return false; + return this.modelService.save(new URI(CodeURI.revive(uri))); } async $tryOpenDocument(uri: UriComponents): Promise { @@ -222,17 +219,6 @@ export class DocumentsMainImpl implements DocumentsMain, Disposable { } } - async $tryCloseDocument(uri: UriComponents): Promise { - const widget = await this.editorManager.getByUri(new URI(CodeURI.revive(uri))); - if (widget) { - await Saveable.save(widget); - widget.close(); - return true; - } - - return false; - } - static toEditorOpenerOptions(shell: ApplicationShell, options?: TextDocumentShowOptions): EditorOpenerOptions | undefined { if (!options) { return undefined; diff --git a/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts b/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts index 125cfad683fa6..00a104e4a30d7 100644 --- a/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts +++ b/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts @@ -30,8 +30,12 @@ import { EditorModelService } from './text-editor-model-service'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { TextEditorMain } from './text-editor-main'; -import { DisposableCollection, Emitter } from '@theia/core'; +import { DisposableCollection, Emitter, URI } from '@theia/core'; import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; +import { SaveableService } from '@theia/core/lib/browser/saveable-service'; +import { TabsMainImpl } from './tabs/tabs-main'; +import { NotebookCellEditorService, NotebookEditorWidgetService } from '@theia/notebook/lib/browser'; +import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor'; export class EditorsAndDocumentsMain implements Disposable { @@ -41,7 +45,8 @@ export class EditorsAndDocumentsMain implements Disposable { private readonly textEditors = new Map(); private readonly modelService: EditorModelService; - private readonly editorService: EditorManager; + private readonly editorManager: EditorManager; + private readonly saveResourceService: SaveableService; private readonly onTextEditorAddEmitter = new Emitter(); private readonly onTextEditorRemoveEmitter = new Emitter(); @@ -57,13 +62,18 @@ export class EditorsAndDocumentsMain implements Disposable { Disposable.create(() => this.textEditors.clear()) ); - constructor(rpc: RPCProtocol, container: interfaces.Container) { + constructor(rpc: RPCProtocol, container: interfaces.Container, tabsMain: TabsMainImpl) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT); - this.editorService = container.get(EditorManager); + this.editorManager = container.get(EditorManager); this.modelService = container.get(EditorModelService); + this.saveResourceService = container.get(SaveableService); - this.stateComputer = new EditorAndDocumentStateComputer(d => this.onDelta(d), this.editorService, this.modelService); + this.stateComputer = new EditorAndDocumentStateComputer(d => this.onDelta(d), + this.editorManager, + container.get(NotebookCellEditorService), + container.get(NotebookEditorWidgetService), + this.modelService, tabsMain); this.toDispose.push(this.stateComputer); this.toDispose.push(this.onTextEditorAddEmitter); this.toDispose.push(this.onTextEditorRemoveEmitter); @@ -167,12 +177,31 @@ export class EditorsAndDocumentsMain implements Disposable { return this.textEditors.get(id); } + async save(uri: URI): Promise { + const editor = await this.editorManager.getByUri(uri); + if (!editor) { + return undefined; + } + return this.saveResourceService.save(editor); + } + + async saveAs(uri: URI): Promise { + const editor = await this.editorManager.getByUri(uri); + if (!editor) { + return undefined; + } + if (!this.saveResourceService.canSaveAs(editor)) { + return undefined; + } + return this.saveResourceService.saveAs(editor); + } + saveAll(includeUntitled?: boolean): Promise { return this.modelService.saveAll(includeUntitled); } hideEditor(id: string): Promise { - for (const editorWidget of this.editorService.all) { + for (const editorWidget of this.editorManager.all) { const monacoEditor = MonacoEditor.get(editorWidget); if (monacoEditor) { if (id === new EditorSnapshot(monacoEditor).id) { @@ -195,21 +224,40 @@ class EditorAndDocumentStateComputer implements Disposable { constructor( private callback: (delta: EditorAndDocumentStateDelta) => void, private readonly editorService: EditorManager, - private readonly modelService: EditorModelService + private readonly cellEditorService: NotebookCellEditorService, + private readonly notebookWidgetService: NotebookEditorWidgetService, + private readonly modelService: EditorModelService, + private readonly tabsMain: TabsMainImpl ) { } listen(): void { if (this.toDispose.disposed) { return; } - this.toDispose.push(this.editorService.onCreated(widget => { + this.toDispose.push(this.editorService.onCreated(async widget => { + await this.tabsMain.waitForWidget(widget); this.onTextEditorAdd(widget); this.update(); })); - this.toDispose.push(this.editorService.onCurrentEditorChanged(() => this.update())); + this.toDispose.push(this.editorService.onCurrentEditorChanged(async widget => { + if (widget) { + await this.tabsMain.waitForWidget(widget); + } + this.update(); + })); this.toDispose.push(this.modelService.onModelAdded(this.onModelAdded, this)); this.toDispose.push(this.modelService.onModelRemoved(() => this.update())); + this.toDispose.push(this.cellEditorService.onDidChangeCellEditors(() => this.update())); + + this.toDispose.push(this.notebookWidgetService.onDidChangeCurrentEditor(() => { + this.currentState = this.currentState && new EditorAndDocumentState( + this.currentState.documents, + this.currentState.editors, + undefined + ); + })); + for (const widget of this.editorService.all) { this.onTextEditorAdd(widget); } @@ -288,6 +336,13 @@ class EditorAndDocumentStateComputer implements Disposable { } } + for (const editor of this.cellEditorService.allCellEditors) { + if (editor.getControl()?.getModel()) { + const editorSnapshot = new EditorSnapshot(editor); + editors.set(editorSnapshot.id, editorSnapshot); + } + }; + const newState = new EditorAndDocumentState(models, editors, activeId); const delta = EditorAndDocumentState.compute(this.currentState, newState); if (!delta.isEmpty) { @@ -354,7 +409,7 @@ class EditorAndDocumentState { class EditorSnapshot { readonly id: string; - constructor(readonly editor: MonacoEditor) { + constructor(readonly editor: MonacoEditor | SimpleMonacoEditor) { this.id = `${editor.getControl().getId()},${editor.getControl().getModel()!.id}`; } } diff --git a/packages/plugin-ext/src/main/browser/env-main.ts b/packages/plugin-ext/src/main/browser/env-main.ts index af9a9fa7335c9..12254b3fc7dd7 100644 --- a/packages/plugin-ext/src/main/browser/env-main.ts +++ b/packages/plugin-ext/src/main/browser/env-main.ts @@ -14,35 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { interfaces } from '@theia/core/shared/inversify'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import { RPCProtocol } from '../../common/rpc-protocol'; -import { EnvMain } from '../../common/plugin-api-rpc'; import { QueryParameters } from '../../common/env'; -import { isWindows, isOSX } from '@theia/core'; -import { OperatingSystem } from '../../plugin/types-impl'; - -export class EnvMainImpl implements EnvMain { - private envVariableServer: EnvVariablesServer; - - constructor(rpc: RPCProtocol, container: interfaces.Container) { - this.envVariableServer = container.get(EnvVariablesServer); - } - - $getEnvVariable(envVarName: string): Promise { - return this.envVariableServer.getValue(envVarName).then(result => result ? result.value : undefined); - } - - async $getClientOperatingSystem(): Promise { - if (isWindows) { - return OperatingSystem.Windows; - } - if (isOSX) { - return OperatingSystem.OSX; - } - return OperatingSystem.Linux; - } -} +export { EnvMainImpl } from '../common/env-main'; /** * Returns query parameters from current page. diff --git a/packages/plugin-ext/src/main/browser/file-system-main-impl.ts b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts index 143a660cd0c12..26ba51941e344 100644 --- a/packages/plugin-ext/src/main/browser/file-system-main-impl.ts +++ b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts @@ -35,9 +35,10 @@ import { UriComponents } from '../../common/uri-components'; import { FileSystemProviderCapabilities, Stat, FileType, FileSystemProviderErrorCode, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, FileWriteOptions, WatchOptions, FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, - FileStat, FileChange, FileOperationError, FileOperationResult + FileStat, FileChange, FileOperationError, FileOperationResult, ReadOnlyMessageFileSystemProvider } from '@theia/filesystem/lib/common/files'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { MarkdownString } from '../../common/plugin-api-rpc-model'; type IDisposable = Disposable; @@ -66,8 +67,8 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable { this._disposables.dispose(); } - $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): void { - this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy)); + $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities, readonlyMessage?: MarkdownString): void { + this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy, readonlyMessage)); } $unregisterProvider(handle: number): void { @@ -163,7 +164,7 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable { } -class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability { +class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, ReadOnlyMessageFileSystemProvider { private readonly _onDidChange = new Emitter(); private readonly _registration: IDisposable; @@ -174,12 +175,15 @@ class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCap readonly capabilities: FileSystemProviderCapabilities; readonly onDidChangeCapabilities: Event = Event.None; + readonly onDidChangeReadOnlyMessage: Event = Event.None; + constructor( fileService: FileService, scheme: string, capabilities: FileSystemProviderCapabilities, private readonly _handle: number, - private readonly _proxy: FileSystemExt + private readonly _proxy: FileSystemExt, + public readonly readOnlyMessage: MarkdownString | undefined = undefined ) { this.capabilities = capabilities; this._registration = fileService.registerProvider(scheme, this); diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 6b6dcbbe6d5fb..4051d8cc9f91e 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -42,7 +42,7 @@ import { injectable, inject } from '@theia/core/shared/inversify'; import { SerializedDocumentFilter, MarkerData, Range, RelatedInformation, MarkerSeverity, DocumentLink, WorkspaceSymbolParams, CodeAction, CompletionDto, - CodeActionProviderDocumentation, InlayHint, InlayHintLabelPart, CodeActionContext, DocumentDropEditProviderMetadata + CodeActionProviderDocumentation, InlayHint, InlayHintLabelPart, CodeActionContext, DocumentDropEditProviderMetadata, SignatureHelpContext } from '../../common/plugin-api-rpc-model'; import { RPCProtocol } from '../../common/rpc-protocol'; import { MonacoLanguages, WorkspaceSymbolProvider } from '@theia/monaco/lib/browser/monaco-languages'; @@ -84,7 +84,7 @@ import { import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model'; import { CodeActionTriggerKind, SnippetString } from '../../plugin/types-impl'; import { DataTransfer } from './data-transfer/data-transfer-type-converters'; -import { VSDataTransfer } from '@theia/monaco-editor-core/esm/vs/base/common/dataTransfer'; +import { IReadonlyVSDataTransfer } from '@theia/monaco-editor-core/esm/vs/base/common/dataTransfer'; import { FileUploadService } from '@theia/filesystem/lib/browser/file-upload-service'; /** @@ -647,7 +647,9 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { protected async provideSignatureHelp(handle: number, model: monaco.editor.ITextModel, position: monaco.Position, token: monaco.CancellationToken, context: monaco.languages.SignatureHelpContext): Promise> { - const value = await this.proxy.$provideSignatureHelp(handle, model.uri, position, context, token); + + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + const value = await this.proxy.$provideSignatureHelp(handle, model.uri, position, context as SignatureHelpContext, token); if (!value) { return undefined; } @@ -749,7 +751,7 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { } protected async provideDocumentDropEdits(handle: number, model: ITextModel, position: monaco.IPosition, - dataTransfer: VSDataTransfer, token: CancellationToken): Promise { + dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { await this.fileUploadService.upload(new URI(), { source: dataTransfer, leaveInTemp: true }); const edit = await this.proxy.$provideDocumentDropEdits(handle, model.uri, position, await DataTransfer.toDataTransferDTO(dataTransfer), token); if (edit) { @@ -759,6 +761,7 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { // yieldTo: edit.yieldTo?.map(yTo => { // return 'mimeType' in yTo ? yTo : { providerId: DocumentOnDropEditAdapter.toInternalProviderId(yTo.extensionId, yTo.providerId) }; // }), + label: 'no label', insertText: edit.insertText instanceof SnippetString ? { snippet: edit.insertText.value } : edit.insertText, additionalEdit: toMonacoWorkspaceEdit(edit?.additionalEdit) }; @@ -888,7 +891,8 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { }; }, resolveInlayHint: async (hint, token): Promise => { - const dto: InlayHintDto = hint; + // need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584 + const dto: InlayHintDto = hint as InlayHintDto; if (typeof dto.cacheId !== 'number') { return hint; } @@ -1430,7 +1434,6 @@ export function toMonacoWorkspaceEdit(data: WorkspaceEditDto | undefined): monac metadata: fileEdit.metadata }; } - // TODO implement WorkspaceNotebookCellEditDto }) }; } diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index a7728f3816015..6467bf99c2b35 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -40,12 +40,9 @@ import { DecorationsMainImpl } from './decorations/decorations-main'; import { ClipboardMainImpl } from './clipboard-main'; import { DocumentsMainImpl } from './documents-main'; import { TextEditorsMainImpl } from './text-editors-main'; -import { EditorManager } from '@theia/editor/lib/browser'; import { EditorModelService } from './text-editor-model-service'; import { OpenerService } from '@theia/core/lib/browser/opener-service'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; -import { MonacoBulkEditService } from '@theia/monaco/lib/browser/monaco-bulk-edit-service'; -import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; import { MainFileSystemEventService } from './main-file-system-event-service'; import { LabelServiceMainImpl } from './label-service-main'; import { TimelineMainImpl } from './timeline-main'; @@ -60,15 +57,14 @@ import { UntitledResourceResolver } from '@theia/core/lib/common/resource'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { TabsMainImpl } from './tabs/tabs-main'; import { NotebooksMainImpl } from './notebooks/notebooks-main'; -import { NotebookService } from '@theia/notebook/lib/browser'; import { LocalizationMainImpl } from './localization-main'; import { NotebookRenderersMainImpl } from './notebooks/notebook-renderers-main'; -import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; import { NotebookEditorsMainImpl } from './notebooks/notebook-editors-main'; import { NotebookDocumentsMainImpl } from './notebooks/notebook-documents-main'; import { NotebookKernelsMainImpl } from './notebooks/notebook-kernels-main'; import { NotebooksAndEditorsMain } from './notebooks/notebook-documents-and-editors-main'; import { TestingMainImpl } from './test-main'; +import { UriMainImpl } from './uri-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const authenticationMain = new AuthenticationMainImpl(rpc, container); @@ -92,31 +88,31 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const preferenceRegistryMain = new PreferenceRegistryMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.PREFERENCE_REGISTRY_MAIN, preferenceRegistryMain); - const editorsAndDocuments = new EditorsAndDocumentsMain(rpc, container); + const tabsMain = new TabsMainImpl(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.TABS_MAIN, tabsMain); + + const editorsAndDocuments = new EditorsAndDocumentsMain(rpc, container, tabsMain); + + const notebookDocumentsMain = new NotebookDocumentsMainImpl(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_MAIN, notebookDocumentsMain); const modelService = container.get(EditorModelService); - const editorManager = container.get(EditorManager); const openerService = container.get(OpenerService); const shell = container.get(ApplicationShell); const untitledResourceResolver = container.get(UntitledResourceResolver); const languageService = container.get(MonacoLanguages); - const documentsMain = new DocumentsMainImpl(editorsAndDocuments, modelService, rpc, editorManager, openerService, shell, untitledResourceResolver, languageService); + const documentsMain = new DocumentsMainImpl(editorsAndDocuments, notebookDocumentsMain, modelService, rpc, + openerService, shell, untitledResourceResolver, languageService); rpc.set(PLUGIN_RPC_CONTEXT.DOCUMENTS_MAIN, documentsMain); - const notebookService = container.get(NotebookService); - const pluginSupport = container.get(HostedPluginSupport); - rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN, new NotebooksMainImpl(rpc, notebookService, pluginSupport)); + rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN, new NotebooksMainImpl(rpc, container, commandRegistryMain)); rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_MAIN, new NotebookRenderersMainImpl(rpc, container)); const notebookEditorsMain = new NotebookEditorsMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_EDITORS_MAIN, notebookEditorsMain); - const notebookDocumentsMain = new NotebookDocumentsMainImpl(rpc, container); - rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_MAIN, notebookDocumentsMain); rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_AND_EDITORS_MAIN, new NotebooksAndEditorsMain(rpc, container, notebookDocumentsMain, notebookEditorsMain)); rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_KERNELS_MAIN, new NotebookKernelsMainImpl(rpc, container)); - const bulkEditService = container.get(MonacoBulkEditService); - const monacoEditorService = container.get(MonacoEditorService); - const editorsMain = new TextEditorsMainImpl(editorsAndDocuments, documentsMain, rpc, bulkEditService, monacoEditorService); + const editorsMain = new TextEditorsMainImpl(editorsAndDocuments, documentsMain, rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.TEXT_EDITORS_MAIN, editorsMain); // start listening only after all clients are subscribed to events @@ -206,9 +202,9 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const commentsMain = new CommentsMainImp(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.COMMENTS_MAIN, commentsMain); - const tabsMain = new TabsMainImpl(rpc, container); - rpc.set(PLUGIN_RPC_CONTEXT.TABS_MAIN, tabsMain); - const localizationMain = new LocalizationMainImpl(container); rpc.set(PLUGIN_RPC_CONTEXT.LOCALIZATION_MAIN, localizationMain); + + const uriMain = new UriMainImpl(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.URI_MAIN, uriMain); } diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts index 2ffb27f5951cd..ae3823e2a7c01 100644 --- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts @@ -31,7 +31,7 @@ import { PluginMenuCommandAdapter, ReferenceCountingSet } from './plugin-menu-co import { ContextKeyExpr } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { PluginSharedStyle } from '../plugin-shared-style'; -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; @injectable() export class MenusContributionPointHandler { @@ -99,19 +99,25 @@ export class MenusContributionPointHandler { const targets = this.getMatchingMenu(contributionPoint as ContributionPoint) ?? [contributionPoint]; const { group, order } = this.parseGroup(item.group); const { submenu, command } = item; - if (submenu) { - targets.forEach(target => toDispose.push(this.menuRegistry.linkSubmenu(target, submenu!, { order, when: item.when }, group))); - } else if (command) { + if (submenu && command) { + console.warn( + `Menu item ${command} from plugin ${plugin.metadata.model.id} contributed both submenu and command. Only command will be registered.` + ); + } + if (command) { toDispose.push(this.commandAdapter.addCommand(command)); targets.forEach(target => { + const node = new ActionMenuNode({ commandId: command, when: item.when, - order, + order }, this.commands); const parent = this.menuRegistry.getMenuNode(target, group); toDispose.push(parent.addNode(node)); }); + } else if (submenu) { + targets.forEach(target => toDispose.push(this.menuRegistry.linkSubmenu(target, submenu!, { order, when: item.when }, group))); } } } catch (error) { diff --git a/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts b/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts index f0cfaba0a22ff..2f1dc3c7e49ab 100644 --- a/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts +++ b/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts @@ -21,12 +21,17 @@ import { URI as CodeUri } from '@theia/core/shared/vscode-uri'; import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection'; import { ScmRepository } from '@theia/scm/lib/browser/scm-repository'; import { ScmService } from '@theia/scm/lib/browser/scm-service'; +import { DirtyDiffWidget } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-widget'; +import { Change, LineRange } from '@theia/scm/lib/browser/dirty-diff/diff-computer'; +import { IChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/legacyLinesDiffComputer'; import { TimelineItem } from '@theia/timeline/lib/common/timeline-model'; import { ScmCommandArg, TimelineCommandArg, TreeViewItemReference } from '../../../common'; +import { TestItemReference, TestMessageArg } from '../../../common/test-types'; import { PluginScmProvider, PluginScmResource, PluginScmResourceGroup } from '../scm-main'; import { TreeViewWidget } from '../view/tree-view-widget'; import { CodeEditorWidgetUtil, codeToTheiaMappings, ContributionPoint } from './vscode-theia-menu-mappings'; import { TAB_BAR_TOOLBAR_CONTEXT_MENU } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { TestItem, TestMessage } from '@theia/test/lib/browser/test-service'; export type ArgumentAdapter = (...args: unknown[]) => unknown[]; @@ -79,6 +84,7 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter { @postConstruct() protected init(): void { const toCommentArgs: ArgumentAdapter = (...args) => this.toCommentArgs(...args); + const toTestMessageArgs: ArgumentAdapter = (...args) => this.toTestMessageArgs(...args); const firstArgOnly: ArgumentAdapter = (...args) => [args[0]]; const noArgs: ArgumentAdapter = () => []; const toScmArgs: ArgumentAdapter = (...args) => this.toScmArgs(...args); @@ -100,9 +106,16 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter { ['scm/resourceGroup/context', toScmArgs], ['scm/resourceState/context', toScmArgs], ['scm/title', () => [this.toScmArg(this.scmService.selectedRepository)]], + ['testing/message/context', toTestMessageArgs], + ['testing/profiles/context', noArgs], + ['scm/change/title', (...args) => this.toScmChangeArgs(...args)], ['timeline/item/context', (...args) => this.toTimelineArgs(...args)], ['view/item/context', (...args) => this.toTreeArgs(...args)], ['view/title', noArgs], + ['webview/context', firstArgOnly], + ['extension/context', noArgs], + ['terminal/context', noArgs], + ['terminal/title/context', noArgs], ]).forEach(([contributionPoint, adapter]) => { if (adapter) { const paths = codeToTheiaMappings.get(contributionPoint); @@ -155,7 +168,27 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter { } protected getArgumentAdapterForMenu(menuPath: MenuPath): ArgumentAdapter | undefined { - return this.argumentAdapters.get(menuPath.join(this.separator)); + let result; + let length = 0; + for (const [key, value] of this.argumentAdapters.entries()) { + const candidate = key.split(this.separator); + if (this.isPrefixOf(candidate, menuPath) && candidate.length > length) { + result = value; + length = candidate.length; + } + } + return result; + } + isPrefixOf(candidate: string[], menuPath: MenuPath): boolean { + if (candidate.length > menuPath.length) { + return false; + } + for (let i = 0; i < candidate.length; i++) { + if (candidate[i] !== menuPath[i]) { + return false; + } + } + return true; } protected addArgumentAdapter(menuPath: MenuPath, adapter: ArgumentAdapter): void { @@ -220,6 +253,41 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter { } } + protected toScmChangeArgs(...args: any[]): any[] { + const arg = args[0]; + if (arg instanceof DirtyDiffWidget) { + const toIChange = (change: Change): IChange => { + const convert = (range: LineRange): [number, number] => { + let startLineNumber; + let endLineNumber; + if (!LineRange.isEmpty(range)) { + startLineNumber = range.start + 1; + endLineNumber = range.end; + } else { + startLineNumber = range.start; + endLineNumber = 0; + } + return [startLineNumber, endLineNumber]; + }; + const { previousRange, currentRange } = change; + const [originalStartLineNumber, originalEndLineNumber] = convert(previousRange); + const [modifiedStartLineNumber, modifiedEndLineNumber] = convert(currentRange); + return { + originalStartLineNumber, + originalEndLineNumber, + modifiedStartLineNumber, + modifiedEndLineNumber + }; + }; + return [ + arg.uri['codeUri'], + arg.changes.map(toIChange), + arg.currentChangeIndex + ]; + } + return []; + } + protected toTimelineArgs(...args: any[]): any[] { const timelineArgs: any[] = []; const arg = args[0]; @@ -229,6 +297,31 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter { return timelineArgs; } + protected toTestMessageArgs(...args: any[]): any[] { + let testItem: TestItem | undefined; + let testMessage: TestMessage | undefined; + for (const arg of args) { + if (TestItem.is(arg)) { + testItem = arg; + } else if (Array.isArray(arg) && TestMessage.is(arg[0])) { + testMessage = arg[0]; + } + } + if (testMessage) { + const testItemReference = (testItem && testItem.controller) ? TestItemReference.create(testItem.controller.id, testItem.path) : undefined; + const testMessageDTO = { + message: testMessage.message, + actual: testMessage.actual, + expected: testMessage.expected, + contextValue: testMessage.contextValue, + location: testMessage.location, + stackTrace: testMessage.stackTrace + }; + return [TestMessageArg.create(testItemReference, testMessageDTO)]; + } + return []; + } + protected toTimelineArg(arg: TimelineItem): TimelineCommandArg { return { timelineHandle: arg.handle, diff --git a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts index 3b15628359f8a..3a519199040f2 100644 --- a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts +++ b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts @@ -26,12 +26,15 @@ import { DebugVariablesWidget } from '@theia/debug/lib/browser/view/debug-variab import { EditorWidget, EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser'; import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution'; import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget'; +import { PLUGIN_SCM_CHANGE_TITLE_MENU } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-widget'; import { TIMELINE_ITEM_CONTEXT_MENU } from '@theia/timeline/lib/browser/timeline-tree-widget'; import { COMMENT_CONTEXT, COMMENT_THREAD_CONTEXT, COMMENT_TITLE } from '../comments/comment-thread-widget'; import { VIEW_ITEM_CONTEXT_MENU } from '../view/tree-view-widget'; -import { WebviewWidget } from '../webview/webview'; +import { WEBVIEW_CONTEXT_MENU, WebviewWidget } from '../webview/webview'; import { EDITOR_LINENUMBER_CONTEXT_MENU } from '@theia/editor/lib/browser/editor-linenumber-contribution'; -import { TEST_VIEW_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-view-contribution'; +import { PLUGIN_TEST_VIEW_TITLE_MENU, TEST_VIEW_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-view-contribution'; +import { TEST_RUNS_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-run-view-contribution'; +import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; export const PLUGIN_EDITOR_TITLE_MENU = ['plugin_editor/title']; export const PLUGIN_EDITOR_TITLE_RUN_MENU = ['plugin_editor/title/run']; @@ -51,14 +54,21 @@ export const implementedVSCodeContributionPoints = [ 'editor/title/run', 'editor/lineNumber/context', 'explorer/context', + 'scm/change/title', 'scm/resourceFolder/context', 'scm/resourceGroup/context', 'scm/resourceState/context', 'scm/title', 'timeline/item/context', 'testing/item/context', + 'testing/message/context', + 'testing/profiles/context', 'view/item/context', - 'view/title' + 'view/title', + 'webview/context', + 'extension/context', + 'terminal/context', + 'terminal/title/context' ] as const; export type ContributionPoint = (typeof implementedVSCodeContributionPoints)[number]; @@ -77,14 +87,22 @@ export const codeToTheiaMappings = new Map([ ['editor/title/run', [PLUGIN_EDITOR_TITLE_RUN_MENU]], ['editor/lineNumber/context', [EDITOR_LINENUMBER_CONTEXT_MENU]], ['explorer/context', [NAVIGATOR_CONTEXT_MENU]], + ['scm/change/title', [PLUGIN_SCM_CHANGE_TITLE_MENU]], ['scm/resourceFolder/context', [ScmTreeWidget.RESOURCE_FOLDER_CONTEXT_MENU]], ['scm/resourceGroup/context', [ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU]], ['scm/resourceState/context', [ScmTreeWidget.RESOURCE_CONTEXT_MENU]], ['scm/title', [PLUGIN_SCM_TITLE_MENU]], ['testing/item/context', [TEST_VIEW_CONTEXT_MENU]], + ['testing/message/context', [TEST_RUNS_CONTEXT_MENU]], + ['testing/profiles/context', [PLUGIN_TEST_VIEW_TITLE_MENU]], ['timeline/item/context', [TIMELINE_ITEM_CONTEXT_MENU]], ['view/item/context', [VIEW_ITEM_CONTEXT_MENU]], ['view/title', [PLUGIN_VIEW_TITLE_MENU]], + ['webview/context', [WEBVIEW_CONTEXT_MENU]], + ['extension/context', [['extensions_context_menu', '3_contribution']]], + ['terminal/context', [TerminalMenus.TERMINAL_CONTRIBUTIONS]], + ['terminal/title/context', [TerminalMenus.TERMINAL_TITLE_CONTRIBUTIONS]] + ]); type CodeEditorWidget = EditorWidget | WebviewWidget; diff --git a/packages/plugin-ext/src/main/browser/message-registry-main.ts b/packages/plugin-ext/src/main/browser/message-registry-main.ts index 5deaf5c747550..53af9a6c37dad 100644 --- a/packages/plugin-ext/src/main/browser/message-registry-main.ts +++ b/packages/plugin-ext/src/main/browser/message-registry-main.ts @@ -15,26 +15,21 @@ // ***************************************************************************** import { interfaces } from '@theia/core/shared/inversify'; -import { MessageService } from '@theia/core/lib/common/message-service'; -import { MessageRegistryMain, MainMessageType, MainMessageOptions, MainMessageItem } from '../../common/plugin-api-rpc'; +import { MainMessageType, MainMessageOptions, MainMessageItem } from '../../common/plugin-api-rpc'; import { ModalNotification, MessageType } from './dialogs/modal-notification'; +import { BasicMessageRegistryMainImpl } from '../common/basic-message-registry-main'; -export class MessageRegistryMainImpl implements MessageRegistryMain { - private readonly messageService: MessageService; - +/** + * Message registry implementation that adds support for the model option via dialog in the browser. + */ +export class MessageRegistryMainImpl extends BasicMessageRegistryMainImpl { constructor(container: interfaces.Container) { - this.messageService = container.get(MessageService); + super(container); } - async $showMessage(type: MainMessageType, message: string, options: MainMessageOptions, actions: MainMessageItem[]): Promise { - const action = await this.doShowMessage(type, message, options, actions); - const handle = action - ? actions.map(a => a.title).indexOf(action) - : undefined; - return handle === undefined && options.modal ? options.onCloseActionHandle : handle; - } + protected override async doShowMessage(type: MainMessageType, message: string, + options: MainMessageOptions, actions: MainMessageItem[]): Promise { - protected async doShowMessage(type: MainMessageType, message: string, options: MainMessageOptions, actions: MainMessageItem[]): Promise { if (options.modal) { const messageType = type === MainMessageType.Error ? MessageType.Error : type === MainMessageType.Warning ? MessageType.Warning : @@ -42,15 +37,7 @@ export class MessageRegistryMainImpl implements MessageRegistryMain { const modalNotification = new ModalNotification(); return modalNotification.showDialog(messageType, message, options, actions); } - switch (type) { - case MainMessageType.Info: - return this.messageService.info(message, ...actions.map(a => a.title)); - case MainMessageType.Warning: - return this.messageService.warn(message, ...actions.map(a => a.title)); - case MainMessageType.Error: - return this.messageService.error(message, ...actions.map(a => a.title)); - } - throw new Error(`Message type '${type}' is not supported yet!`); + return super.doShowMessage(type, message, options, actions); } } diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-and-editors-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-and-editors-main.ts index 166869ef0d79c..06d20cab9b225 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-and-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-and-editors-main.ts @@ -21,9 +21,9 @@ import { Disposable, DisposableCollection } from '@theia/core'; import { interfaces } from '@theia/core/shared/inversify'; import { UriComponents } from '@theia/core/lib/common/uri'; -import { NotebookEditorWidget, NotebookService, NotebookEditorWidgetService } from '@theia/notebook/lib/browser'; +import { NotebookEditorWidget, NotebookService, NotebookEditorWidgetService, NotebookCellEditorService } from '@theia/notebook/lib/browser'; import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; -import { MAIN_RPC_CONTEXT, NotebookDocumentsAndEditorsDelta, NotebookDocumentsAndEditorsMain, NotebookModelAddedData, NotebooksExt } from '../../../common'; +import { MAIN_RPC_CONTEXT, NotebookDocumentsAndEditorsDelta, NotebookDocumentsAndEditorsMain, NotebookEditorAddData, NotebookModelAddedData, NotebooksExt } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { NotebookDto } from './notebook-dto'; import { WidgetManager } from '@theia/core/lib/browser'; @@ -42,7 +42,7 @@ interface NotebookAndEditorDelta { } class NotebookAndEditorState { - static delta(before: NotebookAndEditorState | undefined, after: NotebookAndEditorState): NotebookAndEditorDelta { + static computeDelta(before: NotebookAndEditorState | undefined, after: NotebookAndEditorState): NotebookAndEditorDelta { if (!before) { return { addedDocuments: [...after.documents], @@ -55,7 +55,6 @@ class NotebookAndEditorState { const documentDelta = diffSets(before.documents, after.documents); const editorDelta = diffMaps(before.textEditors, after.textEditors); - const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined; const visibleEditorDelta = diffMaps(before.visibleEditors, after.visibleEditors); return { @@ -63,7 +62,7 @@ class NotebookAndEditorState { removedDocuments: documentDelta.removed.map(e => e.uri.toComponents()), addedEditors: editorDelta.added, removedEditors: editorDelta.removed.map(removed => removed.id), - newActiveEditor: newActiveEditor, + newActiveEditor: after.activeEditor, visibleEditors: visibleEditorDelta.added.length === 0 && visibleEditorDelta.removed.length === 0 ? undefined : [...after.visibleEditors].map(editor => editor[0]) @@ -105,13 +104,16 @@ export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain this.notebookService = container.get(NotebookService); this.notebookEditorService = container.get(NotebookEditorWidgetService); this.WidgetManager = container.get(WidgetManager); + const notebookCellEditorService = container.get(NotebookCellEditorService); + + notebookCellEditorService.onDidChangeFocusedCellEditor(editor => this.proxy.$acceptActiveCellEditorChange(editor?.uri.toString() ?? null), this, this.disposables); this.notebookService.onDidAddNotebookDocument(async () => this.updateState(), this, this.disposables); this.notebookService.onDidRemoveNotebookDocument(async () => this.updateState(), this, this.disposables); // this.WidgetManager.onActiveEditorChanged(() => this.updateState(), this, this.disposables); this.notebookEditorService.onDidAddNotebookEditor(async editor => this.handleEditorAdd(editor), this, this.disposables); this.notebookEditorService.onDidRemoveNotebookEditor(async editor => this.handleEditorRemove(editor), this, this.disposables); - this.notebookEditorService.onDidChangeFocusedEditor(async editor => this.updateState(editor), this, this.disposables); + this.notebookEditorService.onDidChangeCurrentEditor(async editor => this.updateState(editor), this, this.disposables); } dispose(): void { @@ -148,10 +150,8 @@ export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain const editors = new Map(); const visibleEditorsMap = new Map(); - for (const editor of this.notebookEditorService.listNotebookEditors()) { - if (editor.model) { - editors.set(editor.id, editor); - } + for (const editor of this.notebookEditorService.getNotebookEditors()) { + editors.set(editor.id, editor); } const activeNotebookEditor = this.notebookEditorService.focusedEditor; @@ -167,7 +167,7 @@ export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain const notebookEditors = this.WidgetManager.getWidgets(NotebookEditorWidget.ID) as NotebookEditorWidget[]; for (const notebookEditor of notebookEditors) { - if (notebookEditor?.model && editors.has(notebookEditor.id) && notebookEditor.isVisible) { + if (editors.has(notebookEditor.id) && notebookEditor.isVisible) { visibleEditorsMap.set(notebookEditor.id, notebookEditor); } } @@ -176,12 +176,12 @@ export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain new Set(this.notebookService.listNotebookDocuments()), editors, activeEditor, visibleEditorsMap); - await this.onDelta(NotebookAndEditorState.delta(this.currentState, newState)); + await this.onDelta(NotebookAndEditorState.computeDelta(this.currentState, newState)); this.currentState = newState; } private async onDelta(delta: NotebookAndEditorDelta): Promise { - if (NotebooksAndEditorsMain._isDeltaEmpty(delta)) { + if (NotebooksAndEditorsMain.isDeltaEmpty(delta)) { return; } @@ -191,33 +191,35 @@ export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain newActiveEditor: delta.newActiveEditor, visibleEditors: delta.visibleEditors, addedDocuments: delta.addedDocuments.map(NotebooksAndEditorsMain.asModelAddData), - // addedEditors: delta.addedEditors.map(this.asEditorAddData, this), + addedEditors: delta.addedEditors.map(NotebooksAndEditorsMain.asEditorAddData), }; - // send to extension FIRST - await this.proxy.$acceptDocumentsAndEditorsDelta(dto); - - // handle internally + // Handle internally first + // In case the plugin wants to perform documents edits immediately + // we want to make sure that all events have already been setup this.notebookEditorsMain.handleEditorsRemoved(delta.removedEditors); this.notebookDocumentsMain.handleNotebooksRemoved(delta.removedDocuments); this.notebookDocumentsMain.handleNotebooksAdded(delta.addedDocuments); this.notebookEditorsMain.handleEditorsAdded(delta.addedEditors); + + // Send to plugin last + await this.proxy.$acceptDocumentsAndEditorsDelta(dto); } - private static _isDeltaEmpty(delta: NotebookAndEditorDelta): boolean { - if (delta.addedDocuments !== undefined && delta.addedDocuments.length > 0) { + private static isDeltaEmpty(delta: NotebookAndEditorDelta): boolean { + if (delta.addedDocuments?.length) { return false; } - if (delta.removedDocuments !== undefined && delta.removedDocuments.length > 0) { + if (delta.removedDocuments?.length) { return false; } - if (delta.addedEditors !== undefined && delta.addedEditors.length > 0) { + if (delta.addedEditors?.length) { return false; } - if (delta.removedEditors !== undefined && delta.removedEditors.length > 0) { + if (delta.removedEditors?.length) { return false; } - if (delta.visibleEditors !== undefined && delta.visibleEditors.length > 0) { + if (delta.visibleEditors?.length) { return false; } if (delta.newActiveEditor !== undefined) { @@ -235,4 +237,17 @@ export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain cells: e.cells.map(NotebookDto.toNotebookCellDto) }; } + + private static asEditorAddData(notebookEditor: NotebookEditorWidget): NotebookEditorAddData { + const uri = notebookEditor.getResourceUri(); + if (!uri) { + throw new Error('Notebook editor without resource URI'); + } + return { + id: notebookEditor.id, + documentUri: uri.toComponents(), + selections: [{ start: 0, end: 0 }], + visibleRanges: [] + }; + } } diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-main.ts index d989535659219..de36dd66bf491 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-main.ts @@ -14,24 +14,30 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { DisposableCollection } from '@theia/core'; +import { DisposableCollection, Event } from '@theia/core'; import { URI, UriComponents } from '@theia/core/lib/common/uri'; import { interfaces } from '@theia/core/shared/inversify'; import { NotebookModelResolverService } from '@theia/notebook/lib/browser'; import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; import { NotebookCellsChangeType } from '@theia/notebook/lib/common'; -import { MAIN_RPC_CONTEXT, NotebookCellDto, NotebookCellsChangedEventDto, NotebookDataDto, NotebookDocumentsExt, NotebookDocumentsMain } from '../../../common'; +import { NotebookMonacoTextModelService } from '@theia/notebook/lib/browser/service/notebook-monaco-text-model-service'; +import { MAIN_RPC_CONTEXT, NotebookCellsChangedEventDto, NotebookDataDto, NotebookDocumentsExt, NotebookDocumentsMain, NotebookRawContentEventDto } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { NotebookDto } from './notebook-dto'; +import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { NotebookOpenHandler } from '@theia/notebook/lib/browser/notebook-open-handler'; export class NotebookDocumentsMainImpl implements NotebookDocumentsMain { - private readonly disposables = new DisposableCollection(); + protected readonly disposables = new DisposableCollection(); - private readonly proxy: NotebookDocumentsExt; - private readonly documentEventListenersMapping = new Map(); + protected readonly proxy: NotebookDocumentsExt; + protected readonly documentEventListenersMapping = new Map(); - private readonly notebookModelResolverService: NotebookModelResolverService; + protected readonly notebookModelResolverService: NotebookModelResolverService; + + protected readonly notebookMonacoTextModelService: NotebookMonacoTextModelService; + protected readonly notebookOpenHandler: NotebookOpenHandler; constructor( rpc: RPCProtocol, @@ -39,11 +45,17 @@ export class NotebookDocumentsMainImpl implements NotebookDocumentsMain { ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_EXT); this.notebookModelResolverService = container.get(NotebookModelResolverService); + this.notebookOpenHandler = container.get(NotebookOpenHandler); // forward dirty and save events this.disposables.push(this.notebookModelResolverService.onDidChangeDirty(model => this.proxy.$acceptDirtyStateChanged(model.uri.toComponents(), model.isDirty()))); this.disposables.push(this.notebookModelResolverService.onDidSaveNotebook(e => this.proxy.$acceptModelSaved(e))); + this.notebookMonacoTextModelService = container.get(NotebookMonacoTextModelService) as NotebookMonacoTextModelService; + } + + get onDidAddNotebookCellModel(): Event { + return this.notebookMonacoTextModelService.onDidCreateNotebookCellModel; } dispose(): void { @@ -54,23 +66,22 @@ export class NotebookDocumentsMainImpl implements NotebookDocumentsMain { handleNotebooksAdded(notebooks: readonly NotebookModel[]): void { - for (const textModel of notebooks) { - const disposableStore = new DisposableCollection(); - disposableStore.push(textModel.onDidChangeContent(event => { + for (const notebook of notebooks) { + const listener = notebook.onDidChangeContent(events => { const eventDto: NotebookCellsChangedEventDto = { versionId: 1, // TODO implement version ID support rawEvents: [] }; - for (const e of event.rawEvents) { + for (const e of events) { switch (e.kind) { case NotebookCellsChangeType.ModelChange: eventDto.rawEvents.push({ kind: e.kind, changes: e.changes.map(diff => - [diff[0], diff[1], diff[2].map(NotebookDto.toNotebookCellDto)] as [number, number, NotebookCellDto[]]) + ({ ...diff, newItems: diff.newItems.map(NotebookDto.toNotebookCellDto) })) }); break; case NotebookCellsChangeType.Move: @@ -103,23 +114,29 @@ export class NotebookDocumentsMainImpl implements NotebookDocumentsMain { case NotebookCellsChangeType.ChangeCellInternalMetadata: eventDto.rawEvents.push(e); break; + case NotebookCellsChangeType.ChangeDocumentMetadata: + eventDto.rawEvents.push({ + kind: e.kind, + metadata: e.metadata + }); + break; } } - const hasDocumentMetadataChangeEvent = event.rawEvents.find(e => e.kind === NotebookCellsChangeType.ChangeDocumentMetadata); + const hasDocumentMetadataChangeEvent = events.find(e => e.kind === NotebookCellsChangeType.ChangeDocumentMetadata); // using the model resolver service to know if the model is dirty or not. // assuming this is the first listener it can mean that at first the model // is marked as dirty and that another event is fired this.proxy.$acceptModelChanged( - textModel.uri.toComponents(), + notebook.uri.toComponents(), eventDto, - textModel.isDirty(), - hasDocumentMetadataChangeEvent ? textModel.metadata : undefined + notebook.isDirty(), + hasDocumentMetadataChangeEvent ? notebook.metadata : undefined ); - })); + }); - this.documentEventListenersMapping.set(textModel.uri.toString(), disposableStore); + this.documentEventListenersMapping.set(notebook.uri.toString(), new DisposableCollection(listener)); } } @@ -139,27 +156,46 @@ export class NotebookDocumentsMainImpl implements NotebookDocumentsMain { // ref.dispose(); // }); + const uriComponents = ref.uri.toComponents(); // untitled notebooks are dirty by default - this.proxy.$acceptDirtyStateChanged(ref.uri.toComponents(), true); - - // apply content changes... slightly HACKY -> this triggers a change event - // if (options.content) { - // const data = NotebookDto.fromNotebookDataDto(options.content); - // ref.notebook.reset(data.cells, data.metadata, ref.object.notebook.transientOptions); - // } - return ref.uri.toComponents(); + this.proxy.$acceptDirtyStateChanged(uriComponents, true); + + // apply content changes... + if (options.content) { + const data = NotebookDto.fromNotebookDataDto(options.content); + ref.setData(data); + + // Create and send a change events + const rawEvents: NotebookRawContentEventDto[] = []; + if (options.content.cells && options.content.cells.length > 0) { + rawEvents.push({ + kind: NotebookCellsChangeType.ModelChange, + changes: [{ start: 0, deleteCount: 0, newItems: ref.cells.map(NotebookDto.toNotebookCellDto) }] + }); + } + if (options.content.metadata) { + rawEvents.push({ + kind: NotebookCellsChangeType.ChangeDocumentMetadata, + metadata: options.content.metadata + }); + } + if (rawEvents.length > 0) { + this.proxy.$acceptModelChanged(uriComponents, { versionId: 1, rawEvents }, true); + } + } + return uriComponents; } async $tryOpenNotebook(uriComponents: UriComponents): Promise { const uri = URI.fromComponents(uriComponents); + await this.notebookModelResolverService.resolve(uri); return uri.toComponents(); } async $trySaveNotebook(uriComponents: UriComponents): Promise { const uri = URI.fromComponents(uriComponents); - const ref = await this.notebookModelResolverService.resolve(uri); - await ref.save({}); + await ref.save(); ref.dispose(); return true; } diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-dto.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-dto.ts index e6afbb8d79ed6..b80f7b05db948 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-dto.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-dto.ts @@ -18,6 +18,8 @@ import { OS } from '@theia/core'; import * as notebookCommon from '@theia/notebook/lib/common'; import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; import * as rpc from '../../../common'; +import { CellExecutionUpdateType } from '@theia/notebook/lib/common'; +import { CellExecuteUpdate, CellExecutionComplete } from '@theia/notebook/lib/browser'; export namespace NotebookDto { @@ -92,7 +94,7 @@ export namespace NotebookDto { return { handle: cell.handle, uri: cell.uri.toComponents(), - source: cell.text.split(eol), + source: cell.text.split(/\r?\n/g), eol, language: cell.language, cellKind: cell.cellKind, @@ -102,40 +104,28 @@ export namespace NotebookDto { }; } - // export function fromCellExecuteUpdateDto(data: extHostProtocol.ICellExecuteUpdateDto): ICellExecuteUpdate { - // if (data.editType === CellExecutionUpdateType.Output) { - // return { - // editType: data.editType, - // cellHandle: data.cellHandle, - // append: data.append, - // outputs: data.outputs.map(fromNotebookOutputDto) - // }; - // } else if (data.editType === CellExecutionUpdateType.OutputItems) { - // return { - // editType: data.editType, - // append: data.append, - // outputId: data.outputId, - // items: data.items.map(fromNotebookOutputItemDto) - // }; - // } else { - // return data; - // } - // } + export function fromCellExecuteUpdateDto(data: rpc.CellExecuteUpdateDto): CellExecuteUpdate { + if (data.editType === CellExecutionUpdateType.Output) { + return { + editType: data.editType, + cellHandle: data.cellHandle, + append: data.append, + outputs: data.outputs.map(fromNotebookOutputDto) + }; + } else if (data.editType === CellExecutionUpdateType.OutputItems) { + return { + editType: data.editType, + outputId: data.outputId, + append: data.append, + items: data.items.map(fromNotebookOutputItemDto) + }; + } else { + return data; + } + } - // export function fromCellExecuteCompleteDto(data: extHostProtocol.ICellExecutionCompleteDto): ICellExecutionComplete { - // return data; - // } + export function fromCellExecuteCompleteDto(data: rpc.CellExecutionCompleteDto): CellExecutionComplete { + return data; + } - // export function fromCellEditOperationDto(edit: extHostProtocol.ICellEditOperationDto): notebookCommon.ICellEditOperation { - // if (edit.editType === notebookCommon.CellEditType.Replace) { - // return { - // editType: edit.editType, - // index: edit.index, - // count: edit.count, - // cells: edit.cells.map(fromNotebookCellDataDto) - // }; - // } else { - // return edit; - // } - // } } diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-editors-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-editors-main.ts index 3a9a50208a6b9..27f15e8d45aad 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-editors-main.ts @@ -20,16 +20,17 @@ import { UriComponents, URI } from '@theia/core/lib/common/uri'; import { CellRange } from '@theia/notebook/lib/common'; -import { NotebookEditorWidget } from '@theia/notebook/lib/browser'; +import { NotebookEditorWidget, NotebookService } from '@theia/notebook/lib/browser'; import { MAIN_RPC_CONTEXT, NotebookDocumentShowOptions, NotebookEditorRevealType, NotebookEditorsExt, NotebookEditorsMain } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { interfaces } from '@theia/core/shared/inversify'; -import { open, OpenerService } from '@theia/core/lib/browser'; +import { NotebookOpenHandler } from '@theia/notebook/lib/browser/notebook-open-handler'; export class NotebookEditorsMainImpl implements NotebookEditorsMain { protected readonly proxy: NotebookEditorsExt; - protected readonly openerService: OpenerService; + protected readonly notebookService: NotebookService; + protected readonly notebookOpenHandler: NotebookOpenHandler; protected readonly mainThreadEditors = new Map(); @@ -38,23 +39,40 @@ export class NotebookEditorsMainImpl implements NotebookEditorsMain { container: interfaces.Container ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_EDITORS_EXT); - this.openerService = container.get(OpenerService); + this.notebookService = container.get(NotebookService); + this.notebookOpenHandler = container.get(NotebookOpenHandler); } async $tryShowNotebookDocument(uriComponents: UriComponents, viewType: string, options: NotebookDocumentShowOptions): Promise { - const editor = await open(this.openerService, URI.fromComponents(uriComponents), {}); - return (editor as NotebookEditorWidget).id; + const editor = await this.notebookOpenHandler.open(URI.fromComponents(uriComponents), { + notebookType: viewType + }); + await editor.ready; + return editor.id; } $tryRevealRange(id: string, range: CellRange, revealType: NotebookEditorRevealType): Promise { throw new Error('Method not implemented.'); } $trySetSelections(id: string, range: CellRange[]): void { - throw new Error('Method not implemented.'); + if (!this.mainThreadEditors.has(id)) { + throw new Error('Editor not found'); + } + const editor = this.mainThreadEditors.get(id); + editor?.model?.setSelectedCell(editor.model.cells[range[0].start]); } - handleEditorsAdded(editors: readonly NotebookEditorWidget[]): void { + async handleEditorsAdded(editors: readonly NotebookEditorWidget[]): Promise { for (const editor of editors) { this.mainThreadEditors.set(editor.id, editor); + const model = await editor.ready; + model.onDidChangeSelectedCell(e => { + const newCellIndex = e.cell ? model.cells.indexOf(e.cell) : -1; + this.proxy.$acceptEditorPropertiesChanged(editor.id, { + selections: { + selections: newCellIndex >= 0 ? [{ start: newCellIndex, end: newCellIndex }] : [] + } + }); + }); } } diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts index 6f9e735f1f112..fe21cfeaa8ba2 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts @@ -23,13 +23,15 @@ import { UriComponents } from '@theia/core/lib/common/uri'; import { LanguageService } from '@theia/core/lib/browser/language-service'; import { CellExecuteUpdateDto, CellExecutionCompleteDto, MAIN_RPC_CONTEXT, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; -import { CellExecution, NotebookExecutionStateService, NotebookKernelChangeEvent, NotebookKernelService, NotebookService } from '@theia/notebook/lib/browser'; -import { combinedDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle'; +import { + CellExecution, NotebookEditorWidgetService, NotebookExecutionStateService, + NotebookKernelChangeEvent, NotebookKernelService, NotebookService, NotebookKernel as NotebookKernelServiceKernel +} from '@theia/notebook/lib/browser'; import { interfaces } from '@theia/core/shared/inversify'; import { NotebookKernelSourceAction } from '@theia/notebook/lib/common'; -import { NotebookDto } from '../../../plugin/type-converters'; +import { NotebookDto } from './notebook-dto'; -abstract class NotebookKernel { +abstract class NotebookKernel implements NotebookKernelServiceKernel { private readonly onDidChangeEmitter = new Emitter(); private readonly preloads: { uri: URI; provides: readonly string[] }[]; readonly onDidChange: Event = this.onDidChangeEmitter.event; @@ -54,7 +56,7 @@ abstract class NotebookKernel { return this.preloads.map(p => p.provides).flat(); } - constructor(data: NotebookKernelDto, private languageService: LanguageService) { + constructor(public readonly handle: number, data: NotebookKernelDto, private languageService: LanguageService) { this.id = data.id; this.viewType = data.notebookType; this.extensionId = data.extensionId; @@ -65,6 +67,7 @@ abstract class NotebookKernel { this.detail = data.detail; this.supportedLanguages = (data.supportedLanguages && data.supportedLanguages.length > 0) ? data.supportedLanguages : languageService.languages.map(lang => lang.id); this.implementsExecutionOrder = data.supportsExecutionOrder ?? false; + this.localResourceRoot = URI.fromComponents(data.extensionLocation); this.preloads = data.preloads?.map(u => ({ uri: URI.fromComponents(u.uri), provides: u.provides })) ?? []; } @@ -125,6 +128,7 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { private notebookService: NotebookService; private languageService: LanguageService; private notebookExecutionStateService: NotebookExecutionStateService; + private notebookEditorWidgetService: NotebookEditorWidgetService; private readonly executions = new Map(); @@ -138,10 +142,60 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { this.notebookExecutionStateService = container.get(NotebookExecutionStateService); this.notebookService = container.get(NotebookService); this.languageService = container.get(LanguageService); + this.notebookEditorWidgetService = container.get(NotebookEditorWidgetService); + + this.notebookEditorWidgetService.onDidAddNotebookEditor(editor => { + editor.onDidReceiveKernelMessage(async message => { + const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(editor.model!); + if (kernel) { + this.proxy.$acceptKernelMessageFromRenderer(kernel.handle, editor.id, message); + } + }); + }); + this.notebookKernelService.onDidChangeSelectedKernel(e => { + if (e.newKernel) { + const newKernelHandle = Array.from(this.kernels.entries()).find(([_, [kernel]]) => kernel.id === e.newKernel)?.[0]; + if (newKernelHandle !== undefined) { + this.proxy.$acceptNotebookAssociation(newKernelHandle, e.notebook.toComponents(), true); + } + } else { + const oldKernelHandle = Array.from(this.kernels.entries()).find(([_, [kernel]]) => kernel.id === e.oldKernel)?.[0]; + if (oldKernelHandle !== undefined) { + this.proxy.$acceptNotebookAssociation(oldKernelHandle, e.notebook.toComponents(), false); + } + + } + }); } - $postMessage(handle: number, editorId: string | undefined, message: unknown): Promise { - throw new Error('Method not implemented.'); + async $postMessage(handle: number, editorId: string | undefined, message: unknown): Promise { + const tuple = this.kernels.get(handle); + if (!tuple) { + throw new Error('kernel already disposed'); + } + const [kernel] = tuple; + let didSend = false; + for (const editor of this.notebookEditorWidgetService.getNotebookEditors()) { + if (!editor.model) { + continue; + } + if (this.notebookKernelService.getMatchingKernel(editor.model).selected !== kernel) { + // different kernel + continue; + } + if (editorId === undefined) { + // all editors + editor.postKernelMessage(message); + didSend = true; + } else if (editor.id === editorId) { + // selected editors + editor.postKernelMessage(message); + didSend = true; + break; + } + } + return didSend; + } async $addKernel(handle: number, data: NotebookKernelDto): Promise { @@ -153,19 +207,18 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { async cancelNotebookCellExecution(uri: URI, handles: number[]): Promise { await that.proxy.$cancelCells(handle, uri.toComponents(), handles); } - }(data, this.languageService); + }(handle, data, this.languageService); - const listener = this.notebookKernelService.onDidChangeSelectedKernel(e => { - if (e.oldKernel === kernel.id) { - this.proxy.$acceptNotebookAssociation(handle, e.notebook.toComponents(), false); - } else if (e.newKernel === kernel.id) { + // this is for when a kernel is bound to a notebook while being registered + const autobindListener = this.notebookKernelService.onDidChangeSelectedKernel(e => { + if (e.newKernel === kernel.id) { this.proxy.$acceptNotebookAssociation(handle, e.notebook.toComponents(), true); } }); const registration = this.notebookKernelService.registerKernel(kernel); - this.kernels.set(handle, [kernel, combinedDisposable(listener, registration)]); - + this.kernels.set(handle, [kernel, registration]); + autobindListener.dispose(); } $updateKernel(handle: number, data: Partial): void { diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts index 19e3b39d62164..248021fd19c02 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts @@ -14,33 +14,57 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { CancellationToken, DisposableCollection, Emitter } from '@theia/core'; +import { CancellationToken, DisposableCollection, Emitter, URI } from '@theia/core'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; -import { NotebookCellStatusBarItemList, NotebookCellStatusBarItemProvider, NotebookData, TransientOptions } from '@theia/notebook/lib/common'; -import { NotebookService } from '@theia/notebook/lib/browser'; +import { CellEditType, NotebookCellModelResource, NotebookData, NotebookModelResource, TransientOptions } from '@theia/notebook/lib/common'; +import { NotebookService, NotebookWorkspaceEdit } from '@theia/notebook/lib/browser'; import { Disposable } from '@theia/plugin'; -import { MAIN_RPC_CONTEXT, NotebooksExt, NotebooksMain } from '../../../common'; +import { CommandRegistryMain, MAIN_RPC_CONTEXT, NotebooksExt, NotebooksMain, WorkspaceEditDto, WorkspaceNotebookCellEditDto } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { NotebookDto } from './notebook-dto'; -import { UriComponents } from '@theia/core/lib/common/uri'; import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin'; +import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; +import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; +import { interfaces } from '@theia/core/shared/inversify'; +import { + NotebookCellStatusBarItemProvider, + NotebookCellStatusBarItemList, + NotebookCellStatusBarService +} from '@theia/notebook/lib/browser/service/notebook-cell-status-bar-service'; export class NotebooksMainImpl implements NotebooksMain { - private readonly disposables = new DisposableCollection(); + protected readonly disposables = new DisposableCollection(); - private readonly proxy: NotebooksExt; - private readonly notebookSerializer = new Map(); - private readonly notebookCellStatusBarRegistrations = new Map(); + protected notebookService: NotebookService; + protected cellStatusBarService: NotebookCellStatusBarService; + + protected readonly proxy: NotebooksExt; + protected readonly notebookSerializer = new Map(); + protected readonly notebookCellStatusBarRegistrations = new Map(); constructor( rpc: RPCProtocol, - private notebookService: NotebookService, - plugins: HostedPluginSupport + container: interfaces.Container, + commands: CommandRegistryMain ) { + this.notebookService = container.get(NotebookService); + this.cellStatusBarService = container.get(NotebookCellStatusBarService); + const plugins = container.get(HostedPluginSupport); + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT); - notebookService.onWillUseNotebookSerializer(async event => plugins.activateByNotebookSerializer(event)); - notebookService.markReady(); + this.notebookService.onWillUseNotebookSerializer(event => plugins.activateByNotebookSerializer(event)); + this.notebookService.markReady(); + commands.registerArgumentProcessor({ + processArgument: arg => { + if (arg instanceof NotebookModel) { + return NotebookModelResource.create(arg.uri); + } else if (arg instanceof NotebookCellModel) { + return NotebookCellModelResource.create(arg.uri); + } + return arg; + } + }); } dispose(): void { @@ -82,8 +106,8 @@ export class NotebooksMainImpl implements NotebooksMain { async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, viewType: string): Promise { const that = this; const provider: NotebookCellStatusBarItemProvider = { - async provideCellStatusBarItems(uri: UriComponents, index: number, token: CancellationToken): Promise { - const result = await that.proxy.$provideNotebookCellStatusBarItems(handle, uri, index, token); + async provideCellStatusBarItems(notebookUri: URI, index: number, token: CancellationToken): Promise { + const result = await that.proxy.$provideNotebookCellStatusBarItems(handle, notebookUri.toComponents(), index, token); return { items: result?.items ?? [], dispose(): void { @@ -102,8 +126,8 @@ export class NotebooksMainImpl implements NotebooksMain { provider.onDidChangeStatusBarItems = emitter.event; } - // const disposable = this._cellStatusBarService.registerCellStatusBarItemProvider(provider); - // this.notebookCellStatusBarRegistrations.set(handle, disposable); + const disposable = this.cellStatusBarService.registerCellStatusBarItemProvider(provider); + this.notebookCellStatusBarRegistrations.set(handle, disposable); } async $unregisterNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined): Promise { @@ -122,3 +146,14 @@ export class NotebooksMainImpl implements NotebooksMain { } } +export function toNotebookWorspaceEdit(dto: WorkspaceEditDto): NotebookWorkspaceEdit { + return { + edits: dto.edits.map((edit: WorkspaceNotebookCellEditDto) => ({ + resource: URI.fromComponents(edit.resource), + edit: edit.cellEdit.editType === CellEditType.Replace ? { + ...edit.cellEdit, + cells: edit.cellEdit.cells.map(cell => NotebookDto.fromNotebookCellDataDto(cell)) + } : edit.cellEdit + })) + }; +} diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx index 987367cb33abc..9100d5aba52a0 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx @@ -19,37 +19,187 @@ *--------------------------------------------------------------------------------------------*/ import * as React from '@theia/core/shared/react'; -import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; -import { NotebookRendererMessagingService, CellOutputWebview, NotebookRendererRegistry, NotebookEditorWidgetService } from '@theia/notebook/lib/browser'; -import { v4 } from 'uuid'; -import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; +import { inject, injectable, interfaces } from '@theia/core/shared/inversify'; +import { generateUuid } from '@theia/core/lib/common/uuid'; +import { + NotebookRendererMessagingService, CellOutputWebview, NotebookRendererRegistry, + NotebookEditorWidgetService, NotebookKernelService, NotebookEditorWidget, + OutputRenderEvent, + NotebookCellOutputsSplice, + NotebookContentChangedEvent +} from '@theia/notebook/lib/browser'; import { WebviewWidget } from '../../webview/webview'; import { Message, WidgetManager } from '@theia/core/lib/browser'; import { outputWebviewPreload, PreloadContext } from './output-webview-internal'; import { WorkspaceTrustService } from '@theia/workspace/lib/browser'; -import { ChangePreferredMimetypeMessage, FromWebviewMessage, OutputChangedMessage } from './webview-communication'; -import { CellUri, NotebookCellOutputsSplice } from '@theia/notebook/lib/common'; -import { Disposable, DisposableCollection, nls, QuickPickService } from '@theia/core'; +import { CellsChangedMessage, CellsMoved, CellsSpliced, ChangePreferredMimetypeMessage, FromWebviewMessage, OutputChangedMessage } from './webview-communication'; +import { Disposable, DisposableCollection, Emitter, QuickPickService, nls } from '@theia/core'; +import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; +import { NotebookOptionsService, NotebookOutputOptions } from '@theia/notebook/lib/browser/service/notebook-options'; +import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; +import { NotebookCellsChangeType } from '@theia/notebook/lib/common'; import { NotebookCellOutputModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-output-model'; -const CellModel = Symbol('CellModel'); +export const AdditionalNotebookCellOutputCss = Symbol('AdditionalNotebookCellOutputCss'); -export function createCellOutputWebviewContainer(ctx: interfaces.Container, cell: NotebookCellModel): interfaces.Container { +export function createCellOutputWebviewContainer(ctx: interfaces.Container): interfaces.Container { const child = ctx.createChild(); - child.bind(CellModel).toConstantValue(cell); - child.bind(CellOutputWebviewImpl).toSelf().inSingletonScope(); + child.bind(AdditionalNotebookCellOutputCss).toConstantValue(DEFAULT_NOTEBOOK_OUTPUT_CSS); + child.bind(CellOutputWebviewImpl).toSelf(); return child; } +// Should be kept up-to-date with: +// https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping.ts +const mapping: ReadonlyMap = new Map([ + ['theme-font-family', 'vscode-font-family'], + ['theme-font-weight', 'vscode-font-weight'], + ['theme-font-size', 'vscode-font-size'], + ['theme-code-font-family', 'vscode-editor-font-family'], + ['theme-code-font-weight', 'vscode-editor-font-weight'], + ['theme-code-font-size', 'vscode-editor-font-size'], + ['theme-scrollbar-background', 'vscode-scrollbarSlider-background'], + ['theme-scrollbar-hover-background', 'vscode-scrollbarSlider-hoverBackground'], + ['theme-scrollbar-active-background', 'vscode-scrollbarSlider-activeBackground'], + ['theme-quote-background', 'vscode-textBlockQuote-background'], + ['theme-quote-border', 'vscode-textBlockQuote-border'], + ['theme-code-foreground', 'vscode-textPreformat-foreground'], + // Editor + ['theme-background', 'vscode-editor-background'], + ['theme-foreground', 'vscode-editor-foreground'], + ['theme-ui-foreground', 'vscode-foreground'], + ['theme-link', 'vscode-textLink-foreground'], + ['theme-link-active', 'vscode-textLink-activeForeground'], + // Buttons + ['theme-button-background', 'vscode-button-background'], + ['theme-button-hover-background', 'vscode-button-hoverBackground'], + ['theme-button-foreground', 'vscode-button-foreground'], + ['theme-button-secondary-background', 'vscode-button-secondaryBackground'], + ['theme-button-secondary-hover-background', 'vscode-button-secondaryHoverBackground'], + ['theme-button-secondary-foreground', 'vscode-button-secondaryForeground'], + ['theme-button-hover-foreground', 'vscode-button-foreground'], + ['theme-button-focus-foreground', 'vscode-button-foreground'], + ['theme-button-secondary-hover-foreground', 'vscode-button-secondaryForeground'], + ['theme-button-secondary-focus-foreground', 'vscode-button-secondaryForeground'], + // Inputs + ['theme-input-background', 'vscode-input-background'], + ['theme-input-foreground', 'vscode-input-foreground'], + ['theme-input-placeholder-foreground', 'vscode-input-placeholderForeground'], + ['theme-input-focus-border-color', 'vscode-focusBorder'], + // Menus + ['theme-menu-background', 'vscode-menu-background'], + ['theme-menu-foreground', 'vscode-menu-foreground'], + ['theme-menu-hover-background', 'vscode-menu-selectionBackground'], + ['theme-menu-focus-background', 'vscode-menu-selectionBackground'], + ['theme-menu-hover-foreground', 'vscode-menu-selectionForeground'], + ['theme-menu-focus-foreground', 'vscode-menu-selectionForeground'], + // Errors + ['theme-error-background', 'vscode-inputValidation-errorBackground'], + ['theme-error-foreground', 'vscode-foreground'], + ['theme-warning-background', 'vscode-inputValidation-warningBackground'], + ['theme-warning-foreground', 'vscode-foreground'], + ['theme-info-background', 'vscode-inputValidation-infoBackground'], + ['theme-info-foreground', 'vscode-foreground'], + // Notebook: + ['theme-notebook-output-background', 'vscode-notebook-outputContainerBackgroundColor'], + ['theme-notebook-output-border', 'vscode-notebook-outputContainerBorderColor'], + ['theme-notebook-cell-selected-background', 'vscode-notebook-selectedCellBackground'], + ['theme-notebook-symbol-highlight-background', 'vscode-notebook-symbolHighlightBackground'], + ['theme-notebook-diff-removed-background', 'vscode-diffEditor-removedTextBackground'], + ['theme-notebook-diff-inserted-background', 'vscode-diffEditor-insertedTextBackground'], +]); + +const constants: Record = { + 'theme-input-border-width': '1px', + 'theme-button-primary-hover-shadow': 'none', + 'theme-button-secondary-hover-shadow': 'none', + 'theme-input-border-color': 'transparent', +}; + +export const DEFAULT_NOTEBOOK_OUTPUT_CSS = ` +:root { + ${Array.from(mapping.entries()).map(([key, value]) => `--${key}: var(--${value});`).join('\n')} + ${Object.entries(constants).map(([key, value]) => `--${key}: ${value};`).join('\n')} +} + +body { + padding: 0; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +table th, +table td { + border: 1px solid; +} + +table > thead > tr > th { + text-align: left; + border-bottom: 1px solid; +} + +table > thead > tr > th, +table > thead > tr > td, +table > tbody > tr > th, +table > tbody > tr > td { + padding: 5px 10px; +} + +table > tbody > tr + tr > td { + border-top: 1px solid; +} + +table, +thead, +tr, +th, +td, +tbody { + border: none !important; + border-color: transparent; + border-spacing: 0; + border-collapse: collapse; +} + +table, +th, +tr { + vertical-align: middle; + text-align: right; +} + +thead { + font-weight: bold; + background-color: rgba(130, 130, 130, 0.16); +} + +th, +td { + padding: 4px 8px; +} + +tr:nth-child(even) { + background-color: rgba(130, 130, 130, 0.08); +} + +tbody th { + font-weight: normal; +} +`; + +interface CellOutputUpdate extends NotebookCellOutputsSplice { + cellHandle: number +} + @injectable() export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { @inject(NotebookRendererMessagingService) protected readonly messagingService: NotebookRendererMessagingService; - @inject(CellModel) - protected readonly cell: NotebookCellModel; - @inject(WidgetManager) protected readonly widgetManager: WidgetManager; @@ -62,118 +212,314 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { @inject(NotebookEditorWidgetService) protected readonly notebookEditorWidgetService: NotebookEditorWidgetService; + @inject(NotebookKernelService) + protected readonly notebookKernelService: NotebookKernelService; + @inject(QuickPickService) protected readonly quickPickService: QuickPickService; - readonly id = v4(); + @inject(AdditionalNotebookCellOutputCss) + protected readonly additionalOutputCss: string; - protected readonly elementRef = React.createRef(); - protected outputPresentationListeners: DisposableCollection = new DisposableCollection(); + @inject(NotebookOptionsService) + protected readonly notebookOptionsService: NotebookOptionsService; + + // returns the output Height + protected readonly onDidRenderOutputEmitter = new Emitter(); + readonly onDidRenderOutput = this.onDidRenderOutputEmitter.event; + + protected notebook: NotebookModel; + + protected options: NotebookOutputOptions; + + readonly id = generateUuid(); + + protected editor: NotebookEditorWidget | undefined; + + protected element?: HTMLDivElement; // React.createRef(); protected webviewWidget: WebviewWidget; - @postConstruct() - protected async init(): Promise { - this.cell.onDidChangeOutputs(outputChange => this.updateOutput(outputChange)); - this.cell.onDidChangeOutputItems(output => { - this.updateOutput({start: this.cell.outputs.findIndex(o => o.getData().outputId === o.outputId), deleteCount: 1, newOutputs: [output]}); - }); + protected toDispose = new DisposableCollection(); + + protected isDisposed = false; + + async init(notebook: NotebookModel, editor: NotebookEditorWidget): Promise { + this.notebook = notebook; + this.editor = editor; + this.options = this.notebookOptionsService.computeOutputOptions(); + this.toDispose.push(this.notebookOptionsService.onDidChangeOutputOptions(options => { + this.options = options; + this.updateStyles(); + })); this.webviewWidget = await this.widgetManager.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: this.id }); - this.webviewWidget.setContentOptions({ allowScripts: true }); + // this.webviewWidget.parent = this.editor ?? null; + this.webviewWidget.setContentOptions({ + allowScripts: true, + // eslint-disable-next-line max-len + // list taken from https://github.com/microsoft/vscode/blob/a27099233b956dddc2536d4a0d714ab36266d897/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts#L762-L774 + enableCommandUris: [ + 'github-issues.authNow', + 'workbench.extensions.search', + 'workbench.action.openSettings', + '_notebook.selectKernel', + 'jupyter.viewOutput', + 'workbench.action.openLargeOutput', + 'cellOutput.enableScrolling', + ], + }); this.webviewWidget.setHTML(await this.createWebviewContent()); + this.notebook.onDidAddOrRemoveCell(e => { + if (e.newCellIds) { + const newCells = e.newCellIds.map(id => this.notebook.cells.find(cell => cell.handle === id)).filter(cell => !!cell) as NotebookCellModel[]; + newCells.forEach(cell => this.attachCellAndOutputListeners(cell)); + } + }); + this.notebook.cells.forEach(cell => this.attachCellAndOutputListeners(cell)); + + if (this.editor) { + this.toDispose.push(this.editor.onDidPostKernelMessage(message => { + this.webviewWidget.sendMessage({ + type: 'customKernelMessage', + message + }); + })); + + this.toDispose.push(this.editor.onPostRendererMessage(messageObj => { + this.webviewWidget.sendMessage({ + type: 'customRendererMessage', + ...messageObj + }); + })); + + } + this.webviewWidget.onMessage((message: FromWebviewMessage) => { this.handleWebviewMessage(message); }); } + attachCellAndOutputListeners(cell: NotebookCellModel): void { + this.toDispose.push(cell.onDidChangeOutputs(outputChange => this.updateOutputs([{ + newOutputs: outputChange.newOutputs, + start: outputChange.start, + deleteCount: outputChange.deleteCount, + cellHandle: cell.handle + }]))); + this.toDispose.push(cell.onDidChangeOutputItems(output => { + const oldOutputIndex = cell.outputs.findIndex(o => o.outputId === output.outputId); + this.updateOutputs([{ + cellHandle: cell.handle, + newOutputs: [output], + start: oldOutputIndex, + deleteCount: 1 + }]); + })); + this.toDispose.push(cell.onDidCellHeightChange(height => this.setCellHeight(cell, height))); + this.toDispose.push(cell.onDidChangeOutputVisibility(visible => { + this.webviewWidget.sendMessage({ + type: 'outputVisibilityChanged', + cellHandle: cell.handle, + visible + }); + })); + } + render(): React.JSX.Element { - return
    ; + return
    { + if (element) { + this.element = element; + this.attachWebview(); + } + }}>
    ; } attachWebview(): void { - if (this.elementRef.current) { + if (this.element) { this.webviewWidget.processMessage(new Message('before-attach')); - this.elementRef.current.appendChild(this.webviewWidget.node); + this.element.appendChild(this.webviewWidget.node); this.webviewWidget.processMessage(new Message('after-attach')); this.webviewWidget.setIframeHeight(0); } } isAttached(): boolean { - return this.elementRef.current?.contains(this.webviewWidget.node) ?? false; + return this.element?.contains(this.webviewWidget.node) ?? false; } - updateOutput(update: NotebookCellOutputsSplice): void { - if (this.cell.outputs.length > 0) { - if (this.webviewWidget.isHidden) { - this.webviewWidget.show(); - } - - this.outputPresentationListeners.dispose(); - this.outputPresentationListeners = new DisposableCollection(); - for (const output of this.cell.outputs) { - this.outputPresentationListeners.push(output.onRequestOutputPresentationChange(() => this.requestOutputPresentationUpdate(output))); - } + updateOutputs(updates: CellOutputUpdate[]): void { + if (this.webviewWidget.isHidden) { + this.webviewWidget.show(); + } - const updateOutputMessage: OutputChangedMessage = { - type: 'outputChanged', + const updateOutputMessage: OutputChangedMessage = { + type: 'outputChanged', + changes: updates.map(update => ({ + cellHandle: update.cellHandle, newOutputs: update.newOutputs.map(output => ({ id: output.outputId, items: output.outputs.map(item => ({ mime: item.mime, data: item.data.buffer })), metadata: output.metadata })), - deletedOutputIds: this.cell.outputs.slice(update.start, update.start + update.deleteCount).map(output => output.outputId) - }; + start: update.start, + deleteCount: update.deleteCount + })) + }; + + this.webviewWidget.sendMessage(updateOutputMessage); + } + + cellsChanged(cellEvents: NotebookContentChangedEvent[]): void { + const changes: Array = []; + + for (const event of cellEvents) { + if (event.kind === NotebookCellsChangeType.Move) { + changes.push(...event.cells.map((cell, i) => ({ + type: 'cellMoved', + cellHandle: event.cells[0].handle, + toIndex: event.newIdx + i, + } as CellsMoved))); + } else if (event.kind === NotebookCellsChangeType.ModelChange) { + changes.push(...event.changes.map(change => ({ + type: 'cellsSpliced', + start: change.start, + deleteCount: change.deleteCount, + newCells: change.newItems.map(cell => cell.handle) + } as CellsSpliced))); + } + } + + this.webviewWidget.sendMessage({ + type: 'cellsChanged', + changes: changes.filter(e => e) + } as CellsChangedMessage); + } - this.webviewWidget.sendMessage(updateOutputMessage); + setCellHeight(cell: NotebookCellModel, height: number): void { + if (!this.isDisposed) { + this.webviewWidget.sendMessage({ + type: 'cellHeightUpdate', + cellHandle: cell.handle, + cellKind: cell.cellKind, + height + }); } } - private async requestOutputPresentationUpdate(output: NotebookCellOutputModel): Promise { + async requestOutputPresentationUpdate(cellHandle: number, output: NotebookCellOutputModel): Promise { const selectedMime = await this.quickPickService.show( - output.outputs.map(item => ({label: item.mime})), - {description: nls.localizeByDefault('Select mimetype to render for current output' )}); + output.outputs.map(item => ({ label: item.mime })), + { description: nls.localizeByDefault('Select mimetype to render for current output') }); if (selectedMime) { this.webviewWidget.sendMessage({ type: 'changePreferredMimetype', + cellHandle, outputId: output.outputId, mimeType: selectedMime.label } as ChangePreferredMimetypeMessage); } } - private handleWebviewMessage(message: FromWebviewMessage): void { + protected handleWebviewMessage(message: FromWebviewMessage): void { + if (!this.editor) { + throw new Error('No editor found for cell output webview'); + } + switch (message.type) { case 'initialized': - this.updateOutput({newOutputs: this.cell.outputs, start: 0, deleteCount: 0}); + this.updateOutputs(this.notebook.cells.map(cell => ({ + cellHandle: cell.handle, + newOutputs: cell.outputs, + start: 0, + deleteCount: 0 + }))); + this.updateStyles(); break; case 'customRendererMessage': - this.messagingService.getScoped('').postMessage(message.rendererId, message.message); + this.messagingService.getScoped(this.editor.id).postMessage(message.rendererId, message.message); break; case 'didRenderOutput': - this.webviewWidget.setIframeHeight(message.contentHeight + 5); + this.webviewWidget.setIframeHeight(message.bodyHeight); + this.onDidRenderOutputEmitter.fire({ + cellHandle: message.cellHandle, + outputId: message.outputId, + outputHeight: message.outputHeight + }); break; case 'did-scroll-wheel': - this.notebookEditorWidgetService.getNotebookEditor(`notebook:${CellUri.parse(this.cell.uri)?.notebook}`)?.node.scrollBy(message.deltaX, message.deltaY); + this.editor.node.getElementsByClassName('theia-notebook-viewport')[0].children[0].scrollBy(message.deltaX, message.deltaY); + break; + case 'customKernelMessage': + this.editor.recieveKernelMessage(message.message); + break; + case 'inputFocusChanged': + this.editor?.outputInputFocusChanged(message.focused); + break; + case 'cellFocusChanged': + const selectedCell = this.notebook.getCellByHandle(message.cellHandle); + if (selectedCell) { + this.notebook.setSelectedCell(selectedCell); + } + break; + case 'cellHeightRequest': + const cellHeight = this.notebook.getCellByHandle(message.cellHandle)?.cellHeight ?? 0; + this.webviewWidget.sendMessage({ + type: 'cellHeightUpdate', + cellHandle: message.cellHandle, + height: cellHeight + }); + break; + case 'bodyHeightChange': + this.webviewWidget.setIframeHeight(message.height); break; } } + getPreloads(): string[] { + const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(this.notebook); + const kernelPreloads = kernel?.preloadUris.map(uri => uri.toString()) ?? []; + + const staticPreloads = this.notebookRendererRegistry.staticNotebookPreloads + .filter(preload => preload.type === this.notebook.viewType) + .map(preload => preload.entrypoint); + return kernelPreloads.concat(staticPreloads); + } + + protected updateStyles(): void { + this.webviewWidget.sendMessage({ + type: 'notebookStyles', + styles: this.generateStyles() + }); + } + + protected generateStyles(): { [key: string]: string } { + return { + 'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`, + 'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`, + 'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`, + 'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit}px`, + 'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily, + }; + } + private async createWebviewContent(): Promise { const isWorkspaceTrusted = await this.workspaceTrustService.getWorkspaceTrust(); const preloads = this.preloadsScriptString(isWorkspaceTrusted); const content = ` - - - - - - - - - `; + + + + + + + + + + `; return content; } @@ -181,21 +527,22 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { const ctx: PreloadContext = { isWorkspaceTrusted, rendererData: this.notebookRendererRegistry.notebookRenderers, - renderOptions: { // TODO these should be changeable in the settings - lineLimit: 30, - outputScrolling: false, - outputWordWrap: false, - } + renderOptions: { + lineLimit: this.options.outputLineLimit, + outputScrolling: this.options.outputScrolling, + outputWordWrap: this.options.outputWordWrap, + }, + staticPreloadsData: this.getPreloads() }; // TS will try compiling `import()` in webviewPreloads, so use a helper function instead // of using `import(...)` directly return ` const __import = (x) => import(x); - (${outputWebviewPreload})(JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}")))`; + (${outputWebviewPreload})(JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}")))`; } dispose(): void { - this.outputPresentationListeners.dispose(); - this.webviewWidget.dispose(); + this.isDisposed = true; + this.toDispose.dispose(); } } diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts b/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts index b8ac9bdd68939..c52a416b046b6 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts @@ -58,12 +58,28 @@ export interface PreloadContext { readonly isWorkspaceTrusted: boolean; readonly rendererData: readonly webviewCommunication.RendererMetadata[]; readonly renderOptions: RenderOptions; + readonly staticPreloadsData: readonly string[]; +} + +interface KernelPreloadContext { + readonly onDidReceiveKernelMessage: Event; + postKernelMessage(data: unknown): void; +} + +interface KernelPreloadModule { + activate(ctx: KernelPreloadContext): Promise | void; } export async function outputWebviewPreload(ctx: PreloadContext): Promise { const theia = acquireVsCodeApi(); const renderFallbackErrorName = 'vscode.fallbackToNextRenderer'; + document.body.style.overflow = 'hidden'; + const container = document.createElement('div'); + container.id = 'container'; + container.classList.add('widgetarea'); + document.body.appendChild(container); + function createEmitter(listenerChange: (listeners: Set>) => void = () => undefined): EmitterLike { const listeners = new Set>(); return { @@ -98,23 +114,154 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { const settingChange: EmitterLike = createEmitter(); - class Output { + const onDidReceiveKernelMessage = createEmitter(); + + function createKernelContext(): KernelPreloadContext { + return Object.freeze({ + onDidReceiveKernelMessage: onDidReceiveKernelMessage.event, + postKernelMessage: (data: unknown) => { + theia.postMessage({ type: 'customKernelMessage', message: data }); + } + }); + } + + async function runKernelPreload(url: string): Promise { + try { + return activateModuleKernelPreload(url); + } catch (e) { + console.error(e); + throw e; + } + } + + async function activateModuleKernelPreload(url: string): Promise { + const baseUri = window.location.href.replace(/\/webview\/index\.html.*/, ''); + const module: KernelPreloadModule = (await __import(`${baseUri}/${url}`)) as KernelPreloadModule; + if (!module.activate) { + console.error(`Notebook preload '${url}' was expected to be a module but it does not export an 'activate' function`); + return; + } + return module.activate(createKernelContext()); + } + + class OutputCell { + readonly element: HTMLElement; + readonly outputElements: OutputContainer[] = []; + + constructor(public cellHandle: number, cellIndex?: number) { + this.element = document.createElement('div'); + this.element.style.outline = '0'; + + this.element.id = `cellHandle${cellHandle}`; + this.element.classList.add('cell_container'); + + this.element.addEventListener('focusin', e => { + theia.postMessage({ type: 'cellFocusChanged', cellHandle: cellHandle }); + }); + + if (cellIndex && cellIndex < container.children.length) { + container.insertBefore(this.element, container.children[cellIndex]); + } else { + container.appendChild(this.element); + } + this.element = this.element; + + theia.postMessage({ type: 'cellHeightRequest', cellHandle: cellHandle }); + } + + public dispose(): void { + this.element.remove(); + } + + calcTotalOutputHeight(): number { + return this.outputElements.reduce((acc, output) => acc + output.element.clientHeight, 0) + 5; + } + + createOutputElement(index: number, output: webviewCommunication.Output, items: rendererApi.OutputItem[]): OutputContainer { + let outputContainer = this.outputElements.find(o => o.outputId === output.id); + if (!outputContainer) { + outputContainer = new OutputContainer(output, items, this); + this.element.appendChild(outputContainer.containerElement); + this.outputElements.splice(index, 0, outputContainer); + } + + return outputContainer; + } + + public clearOutputs(start: number, deleteCount: number): void { + for (const output of this.outputElements.splice(start, deleteCount)) { + output?.clear(); + output.containerElement.remove(); + } + } + + public show(outputId: string, top: number): void { + const outputContainer = this.outputElements.find(o => o.outputId === outputId); + if (!outputContainer) { + return; + } + } + + public hide(): void { + this.element.style.visibility = 'hidden'; + } + + public updateCellHeight(cellKind: number, height: number): void { + let additionalHeight = 54.5; + additionalHeight -= cells[0] === this ? 2.5 : 0; // first cell + additionalHeight -= this.outputElements.length ? 0 : 5.5; // no outputs + this.element.style.paddingTop = `${height + additionalHeight}px`; + } + + public outputVisibilityChanged(visible: boolean): void { + this.outputElements.forEach(output => { + output.element.style.display = visible ? 'initial' : 'none'; + }); + if (visible) { + this.element.getElementsByClassName('output-hidden')?.[0].remove(); + } else { + const outputHiddenElement = document.createElement('div'); + outputHiddenElement.classList.add('output-hidden'); + outputHiddenElement.style.height = '16px'; + this.element.appendChild(outputHiddenElement); + } + } + + // public updateScroll(request: webviewCommunication.IContentWidgetTopRequest): void { + // this.element.style.top = `${request.cellTop}px`; + + // const outputElement = this.outputElements.get(request.outputId); + // if (outputElement) { + // outputElement.updateScroll(request.outputOffset); + + // if (request.forceDisplay && outputElement.element) { + // // TODO @rebornix @mjbvz, there is a misalignment here. + // // We set output visibility on cell container, other than output container or output node itself. + // outputElement.element.style.visibility = ''; + // } + // } + + // if (request.forceDisplay) { + // this.element.style.visibility = ''; + // } + } + + const cells: OutputCell[] = []; + + class OutputContainer { readonly outputId: string; + readonly cellId: string; renderedItem?: rendererApi.OutputItem; allItems: rendererApi.OutputItem[]; renderer: Renderer; element: HTMLElement; + containerElement: HTMLElement; - constructor(output: webviewCommunication.Output, items: rendererApi.OutputItem[]) { - this.element = document.createElement('div'); - // padding for scrollbars - this.element.style.paddingBottom = '10px'; - this.element.style.paddingRight = '10px'; - this.element.id = output.id; - document.body.appendChild(this.element); - + constructor(output: webviewCommunication.Output, items: rendererApi.OutputItem[], private cell: OutputCell) { + this.outputId = output.id; + this.createHtmlElement(); this.allItems = items; } @@ -132,9 +279,24 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { this.renderer?.disposeOutputItem?.(this.renderedItem?.id); this.element.innerHTML = ''; } - } - const outputs = new Map(); + preferredMimeTypeChange(mimeType: string): void { + this.containerElement.remove(); + this.createHtmlElement(); + this.cell.element.appendChild(this.containerElement); + renderers.render(this.cell, this, mimeType, undefined, new AbortController().signal); + } + + private createHtmlElement(): void { + this.containerElement = document.createElement('div'); + this.containerElement.classList.add('output-container'); + this.element = document.createElement('div'); + this.element.id = this.outputId; + this.element.classList.add('output'); + this.containerElement.appendChild(this.element); + } + + } class Renderer { @@ -160,6 +322,10 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { if (this.rendererApi) { return this.rendererApi; } + + // Preloads need to be loaded before loading renderers. + await kernelPreloads.waitForAllCurrent(); + const baseUri = window.location.href.replace(/\/webview\/index\.html.*/, ''); const rendererModule = await __import(`${baseUri}/${this.data.entrypoint.uri}`) as { activate: rendererApi.ActivationFunction }; this.rendererApi = await rendererModule.activate(this.createRendererContext()); @@ -196,7 +362,9 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { if (this.data.requiresMessaging) { context.onDidReceiveMessage = this.onMessageEvent.event; - context.postMessage = message => theia.postMessage({ type: 'customRendererMessage', rendererId: this.data.id, message }); + context.postMessage = message => { + theia.postMessage({ type: 'customRendererMessage', rendererId: this.data.id, message }); + }; } return Object.freeze(context); @@ -269,7 +437,8 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { this.renderers.get(rendererId)?.disposeOutputItem(outputId); } - public async render(output: Output, preferredMimeType: string | undefined, preferredRendererId: string | undefined, signal: AbortSignal): Promise { + public async render(cell: OutputCell, output: OutputContainer, preferredMimeType: string | undefined, + preferredRendererId: string | undefined, signal: AbortSignal): Promise { const item = output.findItemToRender(preferredMimeType); const primaryRenderer = this.findRenderer(preferredRendererId, item); if (!primaryRenderer) { @@ -280,7 +449,7 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { // Try primary renderer first if (!(await this.doRender(item, output.element, primaryRenderer, signal)).continue) { output.renderer = primaryRenderer; - this.onRenderCompleted(); + this.onRenderCompleted(cell, output); return; } @@ -299,7 +468,7 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { if (renderer) { if (!(await this.doRender(additionalItem, output.element, renderer, signal)).continue) { output.renderer = renderer; - this.onRenderCompleted(); + this.onRenderCompleted(cell, output); return; // We rendered successfully } } @@ -310,21 +479,39 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { this.showRenderError(item, output.element, 'No fallback renderers found or all fallback renderers failed.'); } - private onRenderCompleted(): void { + private onRenderCompleted(cell: OutputCell, output: OutputContainer): void { // we need to check for all images are loaded. Otherwise we can't determine the correct height of the output const images = Array.from(document.images); if (images.length > 0) { - Promise.all(images.filter(img => !img.complete).map(img => new Promise(resolve => { img.onload = img.onerror = resolve; }))).then(() => - theia.postMessage({ type: 'didRenderOutput', contentHeight: document.body.clientHeight })); + Promise.all(images + .filter(img => !img.complete && !img.dataset.waiting) + .map(img => { + img.dataset.waiting = 'true'; // mark to avoid overriding onload a second time + return new Promise(resolve => { img.onload = img.onerror = resolve; }); + })).then(() => { + this.sendDidRenderMessage(cell, output); + new ResizeObserver(() => this.sendDidRenderMessage(cell, output)).observe(cell.element); + }); } else { - theia.postMessage({ type: 'didRenderOutput', contentHeight: document.body.clientHeight }); + this.sendDidRenderMessage(cell, output); + new ResizeObserver(() => this.sendDidRenderMessage(cell, output)).observe(cell.element); } } + private sendDidRenderMessage(cell: OutputCell, output: OutputContainer): void { + theia.postMessage({ + type: 'didRenderOutput', + cellHandle: cell.cellHandle, + outputId: output.outputId, + outputHeight: cell.calcTotalOutputHeight(), + bodyHeight: document.body.clientHeight + }); + } + private async doRender(item: rendererApi.OutputItem, element: HTMLElement, renderer: Renderer, signal: AbortSignal): Promise<{ continue: boolean }> { try { - (await renderer.getOrLoad())?.renderOutputItem(item, element, signal); + await (await renderer.getOrLoad())?.renderOutputItem(item, element, signal); return { continue: false }; // We rendered successfully } catch (e) { if (signal.aborted) { @@ -378,54 +565,115 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { } }(); - function clearOutput(outputId: string): void { - outputs.get(outputId)?.clear(); - outputs.delete(outputId); - document.getElementById(outputId)?.remove(); - } + const kernelPreloads = new class { + private readonly preloads = new Map>(); - function outputsChanged(changedEvent: webviewCommunication.OutputChangedMessage): void { - for (const outputId of changedEvent.deletedOutputIds ?? []) { - clearOutput(outputId); + /** + * Returns a promise that resolves when the given preload is activated. + */ + public waitFor(uri: string): Promise { + return this.preloads.get(uri) || Promise.resolve(new Error(`Preload not ready: ${uri}`)); } - for (const outputData of changedEvent.newOutputs ?? []) { - const apiItems: rendererApi.OutputItem[] = outputData.items.map((item, index) => ({ - id: `${outputData.id}-${index}`, - mime: item.mime, - metadata: outputData.metadata, - data(): Uint8Array { - return item.data; - }, - text(): string { - return new TextDecoder().decode(this.data()); - }, - json(): unknown { - return JSON.parse(this.text()); - }, - blob(): Blob { - return new Blob([this.data()], { type: this.mime }); - }, + /** + * Loads a preload. + * @param uri URI to load from + * @param originalUri URI to show in an error message if the preload is invalid. + */ + public load(uri: string): Promise { + const promise = Promise.all([ + runKernelPreload(uri), + this.waitForAllCurrent(), + ]); + + this.preloads.set(uri, promise); + return promise; + } + + /** + * Returns a promise that waits for all currently-registered preloads to + * activate before resolving. + */ + public waitForAllCurrent(): Promise { + return Promise.all([...this.preloads.values()].map(p => p.catch(err => err))); + } + }; - })); + await Promise.all(ctx.staticPreloadsData.map(preload => kernelPreloads.load(preload))); + + async function outputsChanged(changedEvent: webviewCommunication.OutputChangedMessage): Promise { + for (const cellChange of changedEvent.changes) { + let cell = cells.find(c => c.cellHandle === cellChange.cellHandle); + if (!cell) { + cell = new OutputCell(cellChange.cellHandle); + cells.push(cell); + } - const output = new Output(outputData, apiItems); - outputs.set(outputData.id, output); + cell.clearOutputs(cellChange.start, cellChange.deleteCount); - renderers.render(output, undefined, undefined, new AbortController().signal); + for (const outputData of cellChange.newOutputs ?? []) { + const apiItems: rendererApi.OutputItem[] = outputData.items.map((item, index) => ({ + id: `${outputData.id}-${index}`, + mime: item.mime, + metadata: outputData.metadata, + data(): Uint8Array { + return item.data; + }, + text(): string { + return new TextDecoder().decode(this.data()); + }, + json(): unknown { + return JSON.parse(this.text()); + }, + blob(): Blob { + return new Blob([this.data()], { type: this.mime }); + }, + + })); + const output = cell.createOutputElement(cellChange.start, outputData, apiItems); + + await renderers.render(cell, output, undefined, undefined, new AbortController().signal); + + theia.postMessage({ + type: 'didRenderOutput', + cellHandle: cell.cellHandle, + outputId: outputData.id, + outputHeight: document.getElementById(output.outputId)?.clientHeight ?? 0, + bodyHeight: document.body.clientHeight + }); + + } + } + } + + function cellsChanged(changes: (webviewCommunication.CellsMoved | webviewCommunication.CellsSpliced)[]): void { + for (const change of changes) { + if (change.type === 'cellMoved') { + const currentIndex = cells.findIndex(c => c.cellHandle === change.cellHandle); + const cell = cells[currentIndex]; + cells.splice(change.toIndex, 0, cells.splice(currentIndex, 1)[0]); + if (change.toIndex < cells.length - 1) { + container.insertBefore(cell.element, container.children[change.toIndex + (change.toIndex > currentIndex ? 1 : 0)]); + } else { + container.appendChild(cell.element); + } + } else if (change.type === 'cellsSpliced') { + const deltedCells = cells.splice(change.start, change.deleteCount, ...change.newCells.map((cellHandle, i) => new OutputCell(cellHandle, change.start + i))); + deltedCells.forEach(cell => cell.dispose()); + } } } - function scrollParent(event: WheelEvent): boolean { + function shouldHandleScroll(event: WheelEvent): boolean { for (let node = event.target as Node | null; node; node = node.parentNode) { if (!(node instanceof Element)) { - continue; + return false; } // scroll up if (event.deltaY < 0 && node.scrollTop > 0) { // there is still some content to scroll - return false; + return true; } // scroll down @@ -433,28 +681,36 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { // per https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight // scrollTop is not rounded but scrollHeight and clientHeight are // so we need to check if the difference is less than some threshold - if (node.scrollHeight - node.scrollTop - node.clientHeight > 2) { - return false; + if (node.scrollHeight - node.scrollTop - node.clientHeight < 2) { + continue; + } + + // if the node is not scrollable, we can continue. We don't check the computed style always as it's expensive + if (window.getComputedStyle(node).overflowY === 'hidden' || window.getComputedStyle(node).overflowY === 'visible') { + continue; } + + return true; } } - return true; + return false; } const handleWheel = (event: WheelEvent & { wheelDeltaX?: number; wheelDeltaY?: number; wheelDelta?: number }) => { - if (scrollParent(event)) { - theia.postMessage({ - type: 'did-scroll-wheel', - deltaY: event.deltaY, - deltaX: event.deltaX, - }); + if (event.defaultPrevented || shouldHandleScroll(event)) { + return; } + theia.postMessage({ + type: 'did-scroll-wheel', + deltaY: event.deltaY, + deltaX: event.deltaX, + }); }; window.addEventListener('message', async rawEvent => { const event = rawEvent as ({ data: webviewCommunication.ToWebviewMessage }); - + let cellHandle: number | undefined; switch (event.data.type) { case 'updateRenderers': renderers.updateRendererData(event.data.rendererData); @@ -462,16 +718,83 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { case 'outputChanged': outputsChanged(event.data); break; + case 'cellsChanged': + cellsChanged(event.data.changes); + break; case 'customRendererMessage': renderers.getRenderer(event.data.rendererId)?.receiveMessage(event.data.message); break; case 'changePreferredMimetype': - clearOutput(event.data.outputId); - renderers.render(outputs.get(event.data.outputId)!, event.data.mimeType, undefined, new AbortController().signal); + cellHandle = event.data.cellHandle; + const mimeType = event.data.mimeType; + cells.find(c => c.cellHandle === cellHandle) + ?.outputElements.forEach(o => o.preferredMimeTypeChange(mimeType)); + break; + case 'customKernelMessage': + onDidReceiveKernelMessage.fire(event.data.message); + break; + case 'preload': + const resources = event.data.resources; + for (const uri of resources) { + kernelPreloads.load(uri); + } + break; + case 'notebookStyles': + const documentStyle = window.document.documentElement.style; + + for (let i = documentStyle.length - 1; i >= 0; i--) { + const property = documentStyle[i]; + + // Don't remove properties that the webview might have added separately + if (property && property.startsWith('--notebook-')) { + documentStyle.removeProperty(property); + } + } + + // Re-add new properties + for (const [name, value] of Object.entries(event.data.styles)) { + documentStyle.setProperty(`--${name}`, value); + } + break; + case 'cellHeightUpdate': + cellHandle = event.data.cellHandle; + const cell = cells.find(c => c.cellHandle === cellHandle); + if (cell) { + cell.updateCellHeight(event.data.cellKind, event.data.height); + } + break; + case 'outputVisibilityChanged': + cellHandle = event.data.cellHandle; + cells.find(c => c.cellHandle === cellHandle)?.outputVisibilityChanged(event.data.visible); break; } }); window.addEventListener('wheel', handleWheel); + (document.head as HTMLHeadElement & { originalAppendChild: typeof document.head.appendChild }).originalAppendChild = document.head.appendChild; + (document.head as HTMLHeadElement & { originalAppendChild: typeof document.head.appendChild }).appendChild = function appendChild(node: T): T { + if (node instanceof HTMLScriptElement && node.src.includes('webviewuuid')) { + node.src = node.src.replace('webviewuuid', location.hostname.split('.')[0]); + } + return this.originalAppendChild(node); + }; + + const focusChange = (event: FocusEvent, focus: boolean) => { + if (event.target instanceof HTMLInputElement) { + theia.postMessage({ type: 'inputFocusChanged', focused: focus } as webviewCommunication.InputFocusChange); + } + }; + + window.addEventListener('focusin', (event: FocusEvent) => focusChange(event, true)); + + window.addEventListener('focusout', (event: FocusEvent) => focusChange(event, false)); + + new ResizeObserver(() => { + theia.postMessage({ + type: 'bodyHeightChange', + height: document.body.clientHeight + } as webviewCommunication.BodyHeightChange); + }).observe(document.body); + theia.postMessage({ type: 'initialized' }); } diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts b/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts index 4fee027832c22..040ef038832f5 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts @@ -36,19 +36,87 @@ export interface UpdateRenderersMessage { readonly rendererData: readonly RendererMetadata[]; } +export interface CellOutputChange { + readonly cellHandle: number; + readonly newOutputs?: Output[]; + readonly start: number; + readonly deleteCount: number; +} + export interface OutputChangedMessage { readonly type: 'outputChanged'; - readonly newOutputs?: Output[]; - readonly deletedOutputIds?: string[]; + changes: CellOutputChange[]; } export interface ChangePreferredMimetypeMessage { readonly type: 'changePreferredMimetype'; + readonly cellHandle: number; readonly outputId: string; readonly mimeType: string; } -export type ToWebviewMessage = UpdateRenderersMessage | OutputChangedMessage | ChangePreferredMimetypeMessage | CustomRendererMessage; +export interface KernelMessage { + readonly type: 'customKernelMessage'; + readonly message: unknown; +} + +export interface PreloadMessage { + readonly type: 'preload'; + readonly resources: string[]; +} + +export interface notebookStylesMessage { + readonly type: 'notebookStyles'; + styles: Record; +} + +export interface CellHeigthsMessage { + type: 'cellHeigths'; + cellHeigths: Record; +} + +export interface CellsMoved { + type: 'cellMoved'; + cellHandle: number; + toIndex: number; +} + +export interface CellsSpliced { + type: 'cellsSpliced'; + start: number; + deleteCount: number; + newCells: number[]; +} + +export interface CellsChangedMessage { + type: 'cellsChanged'; + changes: Array; +} + +export interface CellHeightUpdateMessage { + type: 'cellHeightUpdate'; + cellKind: number; + cellHandle: number; + height: number; +} + +export interface OutputVisibilityChangedMessage { + type: 'outputVisibilityChanged'; + cellHandle: number; + visible: boolean; +} + +export type ToWebviewMessage = UpdateRenderersMessage + | OutputChangedMessage + | ChangePreferredMimetypeMessage + | CustomRendererMessage + | KernelMessage + | PreloadMessage + | notebookStylesMessage + | CellHeigthsMessage + | CellHeightUpdateMessage + | CellsChangedMessage + | OutputVisibilityChangedMessage; export interface WebviewInitialized { readonly type: 'initialized'; @@ -56,7 +124,10 @@ export interface WebviewInitialized { export interface OnDidRenderOutput { readonly type: 'didRenderOutput'; - contentHeight: number; + cellHandle: number; + outputId: string; + outputHeight: number; + bodyHeight: number; } export interface WheelMessage { @@ -65,7 +136,35 @@ export interface WheelMessage { readonly deltaX: number; } -export type FromWebviewMessage = WebviewInitialized | OnDidRenderOutput | WheelMessage | CustomRendererMessage; +export interface InputFocusChange { + readonly type: 'inputFocusChanged'; + readonly focused: boolean; +} + +export interface CellOuputFocus { + readonly type: 'cellFocusChanged'; + readonly cellHandle: number; +} + +export interface CellHeightRequest { + readonly type: 'cellHeightRequest'; + readonly cellHandle: number; +} + +export interface BodyHeightChange { + readonly type: 'bodyHeightChange'; + readonly height: number; +} + +export type FromWebviewMessage = WebviewInitialized + | OnDidRenderOutput + | WheelMessage + | CustomRendererMessage + | KernelMessage + | InputFocusChange + | CellOuputFocus + | CellHeightRequest + | BodyHeightChange; export interface Output { id: string diff --git a/packages/plugin-ext/src/main/browser/notification-main.ts b/packages/plugin-ext/src/main/browser/notification-main.ts index 90ab72eb7203c..eb3379f866099 100644 --- a/packages/plugin-ext/src/main/browser/notification-main.ts +++ b/packages/plugin-ext/src/main/browser/notification-main.ts @@ -14,73 +14,13 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { NotificationExt, NotificationMain, MAIN_RPC_CONTEXT } from '../../common'; -import { ProgressService, Progress, ProgressMessage } from '@theia/core/lib/common'; +import { MAIN_RPC_CONTEXT } from '../../common'; import { interfaces } from '@theia/core/shared/inversify'; import { RPCProtocol } from '../../common/rpc-protocol'; -import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; - -export class NotificationMainImpl implements NotificationMain, Disposable { - private readonly progressService: ProgressService; - private readonly progressMap = new Map(); - private readonly progress2Work = new Map(); - private readonly proxy: NotificationExt; - - protected readonly toDispose = new DisposableCollection( - Disposable.create(() => { /* mark as not disposed */ }) - ); +import { BasicNotificationMainImpl } from '../common/basic-notification-main'; +export class NotificationMainImpl extends BasicNotificationMainImpl { constructor(rpc: RPCProtocol, container: interfaces.Container) { - this.progressService = container.get(ProgressService); - this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTIFICATION_EXT); - } - - dispose(): void { - this.toDispose.dispose(); - } - - async $startProgress(options: NotificationMain.StartProgressOptions): Promise { - const onDidCancel = () => { - // If the map does not contain current id, it has already stopped and should not be cancelled - if (this.progressMap.has(id)) { - this.proxy.$acceptProgressCanceled(id); - } - }; - - const progressMessage = this.mapOptions(options); - const progress = await this.progressService.showProgress(progressMessage, onDidCancel); - const id = progress.id; - this.progressMap.set(id, progress); - this.progress2Work.set(id, 0); - if (this.toDispose.disposed) { - this.$stopProgress(id); - } else { - this.toDispose.push(Disposable.create(() => this.$stopProgress(id))); - } - return id; - } - protected mapOptions(options: NotificationMain.StartProgressOptions): ProgressMessage { - const { title, location, cancellable } = options; - return { text: title, options: { location, cancelable: cancellable } }; - } - - $stopProgress(id: string): void { - const progress = this.progressMap.get(id); - - if (progress) { - this.progressMap.delete(id); - this.progress2Work.delete(id); - progress.cancel(); - } - } - - $updateProgress(id: string, item: NotificationMain.ProgressReport): void { - const progress = this.progressMap.get(id); - if (!progress) { - return; - } - const done = Math.min((this.progress2Work.get(id) || 0) + (item.increment || 0), 100); - this.progress2Work.set(id, done); - progress.report({ message: item.message, work: done ? { done, total: 100 } : undefined }); + super(rpc, container, MAIN_RPC_CONTEXT.NOTIFICATION_EXT); } } diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index edd4584b67d73..89cd8b4f22fe8 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -47,13 +47,13 @@ import { PluginIconService } from './plugin-icon-service'; import { PluginIconThemeService } from './plugin-icon-theme-service'; import { ContributionProvider } from '@theia/core/lib/common'; import * as monaco from '@theia/monaco-editor-core'; -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; import { ContributedTerminalProfileStore, TerminalProfileStore } from '@theia/terminal/lib/browser/terminal-profile-service'; import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; import { PluginTerminalRegistry } from './plugin-terminal-registry'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { LanguageService } from '@theia/core/lib/browser/language-service'; +import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; @injectable() export class PluginContributionHandler { @@ -288,7 +288,7 @@ export class PluginContributionHandler { if (contributions.customEditors) { for (const customEditor of contributions.customEditors) { pushContribution(`customEditors.${customEditor.viewType}`, - () => this.customEditorRegistry.registerCustomEditor(customEditor) + () => this.customEditorRegistry.registerCustomEditor(customEditor, plugin) ); } } @@ -434,7 +434,7 @@ export class PluginContributionHandler { if (contributions.notebooks) { for (const notebook of contributions.notebooks) { pushContribution(`notebook.${notebook.type}`, - () => this.notebookTypeRegistry.registerNotebookType(notebook) + () => this.notebookTypeRegistry.registerNotebookType(notebook, plugin.metadata.model.displayName) ); } } @@ -447,6 +447,14 @@ export class PluginContributionHandler { } } + if (contributions.notebookPreload) { + for (const preload of contributions.notebookPreload) { + pushContribution(`notebookPreloads.${preload.type}:${preload.entrypoint}`, + () => this.notebookRendererRegistry.registerStaticNotebookPreload(preload.type, preload.entrypoint, PluginPackage.toPluginUrl(plugin.metadata.model, '')) + ); + } + } + return toDispose; } @@ -455,7 +463,7 @@ export class PluginContributionHandler { return Disposable.NULL; } const toDispose = new DisposableCollection(); - for (const { iconUrl, themeIcon, command, category, title, originalTitle, enablement } of contribution.commands) { + for (const { iconUrl, themeIcon, command, category, shortTitle, title, originalTitle, enablement } of contribution.commands) { const reference = iconUrl && this.style.toIconClass(iconUrl); const icon = themeIcon && ThemeIcon.fromString(themeIcon); let iconClass; @@ -465,7 +473,7 @@ export class PluginContributionHandler { } else if (icon) { iconClass = ThemeIcon.asClassName(icon); } - toDispose.push(this.registerCommand({ id: command, category, label: title, originalLabel: originalTitle, iconClass }, enablement)); + toDispose.push(this.registerCommand({ id: command, category, shortTitle, label: title, originalLabel: originalTitle, iconClass }, enablement)); } return toDispose; } diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index e16d944c95926..ef2530a437d21 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -21,9 +21,12 @@ import '../../../src/main/browser/style/comments.css'; import { ContainerModule } from '@theia/core/shared/inversify'; import { FrontendApplicationContribution, WidgetFactory, bindViewContribution, - ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeWidget, LabelProviderContribution + ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeWidget, LabelProviderContribution, LabelProvider, + UndoRedoHandler, DiffUris, Navigatable, SplitWidget, + noopWidgetStatusBarContribution, + WidgetStatusBarContribution } from '@theia/core/lib/browser'; -import { MaybePromise, CommandContribution, ResourceResolver, bindContributionProvider } from '@theia/core/lib/common'; +import { MaybePromise, CommandContribution, ResourceResolver, bindContributionProvider, URI, generateUuid } from '@theia/core/lib/common'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging'; import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; import { HostedPluginWatcher } from '../../hosted/browser/hosted-plugin-watcher'; @@ -47,7 +50,6 @@ import { PluginDebugService } from './debug/plugin-debug-service'; import { DebugService } from '@theia/debug/lib/common/debug-service'; import { PluginSharedStyle } from './plugin-shared-style'; import { SelectionProviderCommandContribution } from './selection-provider-command'; -import { ViewColumnService } from './view-column-service'; import { ViewContextKeyService } from './view/view-context-key-service'; import { PluginViewWidget, PluginViewWidgetIdentifier } from './view/plugin-view-widget'; import { TreeViewWidgetOptions, VIEW_ITEM_CONTEXT_MENU, PluginTree, TreeViewWidget, PluginTreeModel } from './view/tree-view-widget'; @@ -67,7 +69,6 @@ import { CommentsService, PluginCommentService } from './comments/comments-servi import { CommentingRangeDecorator } from './comments/comments-decorator'; import { CommentsContribution } from './comments/comments-contribution'; import { CommentsContextKeyService } from './comments/comments-context-key-service'; -import { CustomEditorContribution } from './custom-editors/custom-editor-contribution'; import { PluginCustomEditorRegistry } from './custom-editors/plugin-custom-editor-registry'; import { CustomEditorWidgetFactory } from '../browser/custom-editors/custom-editor-widget-factory'; import { CustomEditorWidget } from './custom-editors/custom-editor-widget'; @@ -87,7 +88,9 @@ import { LanguagePackService, languagePackServicePath } from '../../common/langu import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { CellOutputWebviewFactory } from '@theia/notebook/lib/browser'; import { CellOutputWebviewImpl, createCellOutputWebviewContainer } from './notebooks/renderers/cell-output-webview'; -import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; +import { ArgumentProcessorContribution } from './command-registry-main'; +import { WebviewSecondaryWindowSupport } from './webview/webview-secondary-window-support'; +import { CustomEditorUndoRedoHandler } from './custom-editors/custom-editor-undo-redo-handler'; export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -185,16 +188,37 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(WebviewWidgetFactory).toDynamicValue(ctx => new WebviewWidgetFactory(ctx.container)).inSingletonScope(); bind(WidgetFactory).toService(WebviewWidgetFactory); bind(WebviewContextKeys).toSelf().inSingletonScope(); + bind(WebviewSecondaryWindowSupport).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(WebviewSecondaryWindowSupport); bind(FrontendApplicationContribution).toService(WebviewContextKeys); - - bind(CustomEditorContribution).toSelf().inSingletonScope(); - bind(CommandContribution).toService(CustomEditorContribution); + bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(WebviewWidget)); bind(PluginCustomEditorRegistry).toSelf().inSingletonScope(); bind(CustomEditorService).toSelf().inSingletonScope(); bind(CustomEditorWidget).toSelf(); bind(CustomEditorWidgetFactory).toDynamicValue(ctx => new CustomEditorWidgetFactory(ctx.container)).inSingletonScope(); bind(WidgetFactory).toService(CustomEditorWidgetFactory); + bind(CustomEditorUndoRedoHandler).toSelf().inSingletonScope(); + bind(UndoRedoHandler).toService(CustomEditorUndoRedoHandler); + + bind(WidgetFactory).toDynamicValue(ctx => ({ + id: CustomEditorWidget.SIDE_BY_SIDE_FACTORY_ID, + createWidget: (arg: { uri: string, viewType: string }) => { + const uri = new URI(arg.uri); + const [leftUri, rightUri] = DiffUris.decode(uri); + const navigatable: Navigatable = { + getResourceUri: () => rightUri, + createMoveToUri: resourceUri => DiffUris.encode(leftUri, rightUri.withPath(resourceUri.path)) + }; + const widget = new SplitWidget({ navigatable }); + widget.id = arg.viewType + '.side-by-side:' + generateUuid(); + const labelProvider = ctx.container.get(LabelProvider); + widget.title.label = labelProvider.getName(uri); + widget.title.iconClass = labelProvider.getIcon(uri); + widget.title.closable = true; + return widget; + } + })).inSingletonScope(); bind(PluginViewWidget).toSelf(); bind(WidgetFactory).toDynamicValue(({ container }) => ({ @@ -239,8 +263,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(PluginDebugSessionContributionRegistry).toSelf().inSingletonScope(); rebind(DebugSessionContributionRegistry).toService(PluginDebugSessionContributionRegistry); - bind(ViewColumnService).toSelf().inSingletonScope(); - bind(CommentsService).to(PluginCommentService).inSingletonScope(); bind(CommentingRangeDecorator).toSelf().inSingletonScope(); bind(CommentsContribution).toSelf().inSingletonScope(); @@ -262,7 +284,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { return provider.createProxy(languagePackServicePath); }).inSingletonScope(); - bind(CellOutputWebviewFactory).toFactory(ctx => async (cell: NotebookCellModel) => - createCellOutputWebviewContainer(ctx.container, cell).getAsync(CellOutputWebviewImpl) + bind(CellOutputWebviewFactory).toFactory(ctx => () => + createCellOutputWebviewContainer(ctx.container).get(CellOutputWebviewImpl) ); + bindContributionProvider(bind, ArgumentProcessorContribution); + }); diff --git a/packages/plugin-ext/src/main/browser/plugin-icon-service.ts b/packages/plugin-ext/src/main/browser/plugin-icon-service.ts index caef198ea31c0..18ad070c67a99 100644 --- a/packages/plugin-ext/src/main/browser/plugin-icon-service.ts +++ b/packages/plugin-ext/src/main/browser/plugin-icon-service.ts @@ -14,18 +14,13 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { asCSSPropertyValue } from '@theia/monaco-editor-core/esm/vs/base/browser/dom'; import { Endpoint } from '@theia/core/lib/browser'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { getIconRegistry } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/iconRegistry'; import { inject, injectable } from '@theia/core/shared/inversify'; import { URI } from '@theia/core/shared/vscode-uri'; -import { IconFontDefinition, IconContribution as Icon } from '@theia/core/lib/browser/icon-registry'; import { MonacoIconRegistry } from '@theia/monaco/lib/browser/monaco-icon-registry'; import * as path from 'path'; import { IconContribution, DeployedPlugin, IconDefinition } from '../../common/plugin-protocol'; -import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; -import { UnthemedProductIconTheme } from '@theia/monaco-editor-core/esm/vs/platform/theme/browser/iconsStyleSheet'; @injectable() export class PluginIconService implements Disposable { @@ -45,29 +40,19 @@ export class PluginIconService implements Disposable { } else { this.registerRegularIcon(contribution, defaultIcon.id); } - this.updateStyle(contribution); return Disposable.NULL; } - updateStyle(contribution: IconContribution): void { - this.updateStyleElement(); - const css = this.getCSS(contribution); - if (css) { - this.styleElement.innerText = css; - } - const toRemoveStyleElement = Disposable.create(() => this.styleElement.remove()); - this.toDispose.push(toRemoveStyleElement); - } - dispose(): void { this.toDispose.dispose(); } protected registerFontIcon(contribution: IconContribution, defaultIcon: IconDefinition): void { - const location = defaultIcon.location; - const format = getFileExtension(location); - const fontId = getFontId(contribution.extensionId, location); - const definition = this.iconRegistry.registerIconFont(fontId, { src: [{ location: URI.file(location), format }] }); + const location = this.toPluginUrl(contribution.extensionId, getIconRelativePath(URI.parse(defaultIcon.location).path)); + const format = getFileExtension(location.path); + const fontId = getFontId(contribution.extensionId, location.path); + + const definition = this.iconRegistry.registerIconFont(fontId, { src: [{ location: location, format }] }); this.iconRegistry.registerIcon(contribution.id, { fontCharacter: defaultIcon.fontCharacter, font: { @@ -81,59 +66,10 @@ export class PluginIconService implements Disposable { this.iconRegistry.registerIcon(contribution.id, { id: defaultIconId }, contribution.description); } - protected updateStyleElement(): void { - if (!this.styleElement) { - const styleElement = document.createElement('style'); - styleElement.type = 'text/css'; - styleElement.media = 'screen'; - styleElement.id = 'contributedIconsStyles'; - document.head.appendChild(styleElement); - this.styleElement = styleElement; - } - } - protected getCSS(iconContribution: IconContribution, themeService?: IThemeService): string | undefined { - const iconRegistry = getIconRegistry(); - const productIconTheme = themeService ? themeService.getProductIconTheme() : new UnthemedProductIconTheme(); - const usedFontIds: { [id: string]: IconFontDefinition } = {}; - const formatIconRule = (contribution: Icon): string | undefined => { - const definition = productIconTheme.getIcon(contribution); - if (!definition) { - return undefined; - } - const fontContribution = definition.font; - if (fontContribution) { - usedFontIds[fontContribution.id] = fontContribution.definition; - return `.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; font-family: ${asCSSPropertyValue(iconContribution.extensionId)}; }`; - } - // default font (codicon) - return `.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; }`; - }; - - const rules = []; - for (const contribution of iconRegistry.getIcons()) { - const rule = formatIconRule(contribution); - if (rule) { - rules.push(rule); - } - } - for (const id in usedFontIds) { - if (id) { - const definition = usedFontIds[id]; - const fontWeight = definition.weight ? `font-weight: ${definition.weight};` : ''; - const fontStyle = definition.style ? `font-style: ${definition.style};` : ''; - const src = definition.src.map(icon => - `${this.toPluginUrl(iconContribution.extensionId, getIconRelativePath(icon.location.path))} format('${icon.format}')`) - .join(', '); - rules.push(`@font-face { src: ${src}; font-family: ${asCSSPropertyValue(iconContribution.extensionId)};${fontWeight}${fontStyle} font-display: block; }`); - } - } - return rules.join('\n'); - } - - protected toPluginUrl(id: string, relativePath: string): string { - return `url('${new Endpoint({ + protected toPluginUrl(id: string, relativePath: string): URI { + return URI.from(new Endpoint({ path: `hostedPlugin/${this.formatExtensionId(id)}/${encodeURIComponent(relativePath)}` - }).getRestUrl().toString()}')`; + }).getRestUrl().toComponents()); } protected formatExtensionId(id: string): string { diff --git a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts index 8fa32a52d9fba..760de38475cad 100644 --- a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts +++ b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts @@ -125,8 +125,9 @@ export class PluginSharedStyle { toDispose.push(this.insertRule('.' + iconClass + '::before', theme => ` content: ""; background-position: 2px; - width: ${size}'px'; - height: ${size}'px'; + display: block; + width: ${size}px; + height: ${size}px; background: center no-repeat url("${theme.type === 'light' ? lightIconUrl : darkIconUrl}"); background-size: ${size}px; `)); diff --git a/packages/plugin-ext/src/main/browser/quick-open-main.ts b/packages/plugin-ext/src/main/browser/quick-open-main.ts index c65d81296886e..4c06ebdc8aa5f 100644 --- a/packages/plugin-ext/src/main/browser/quick-open-main.ts +++ b/packages/plugin-ext/src/main/browser/quick-open-main.ts @@ -23,30 +23,32 @@ import { QuickOpenMain, MAIN_RPC_CONTEXT, TransferInputBox, - TransferQuickPickItems, + TransferQuickPickItem, TransferQuickInput, TransferQuickInputButton, - TransferQuickPickItemValue + TransferQuickPickOptions } from '../../common/plugin-api-rpc'; import { InputOptions, - PickOptions, + QuickInput, QuickInputButton, QuickInputButtonHandle, - QuickInputService + QuickInputService, + QuickPickItem, + QuickPickItemOrSeparator, + codiconArray } from '@theia/core/lib/browser'; import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { MonacoQuickInputService } from '@theia/monaco/lib/browser/monaco-quick-input-service'; import { QuickInputButtons } from '../../plugin/types-impl'; -import { getIconPathOrClass } from '../../plugin/quick-open'; -import * as monaco from '@theia/monaco-editor-core'; -import { IQuickPickItem, IQuickInput } from '@theia/monaco-editor-core/esm/vs/base/parts/quickinput/common/quickInput'; -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; +import { PluginSharedStyle } from './plugin-shared-style'; +import { QuickPickSeparator } from '@theia/core'; export interface QuickInputSession { - input: IQuickInput; - handlesToItems: Map; + input: QuickInput; + handlesToItems: Map; } export class QuickOpenMainImpl implements QuickOpenMain, Disposable { @@ -54,8 +56,9 @@ export class QuickOpenMainImpl implements QuickOpenMain, Disposable { private quickInputService: QuickInputService; private proxy: QuickOpenExt; private delegate: MonacoQuickInputService; + private sharedStyle: PluginSharedStyle; private readonly items: Record = {}; @@ -65,39 +68,88 @@ export class QuickOpenMainImpl implements QuickOpenMain, Disposable { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.QUICK_OPEN_EXT); this.delegate = container.get(MonacoQuickInputService); this.quickInputService = container.get(QuickInputService); + this.sharedStyle = container.get(PluginSharedStyle); } dispose(): void { this.toDispose.dispose(); } - async $show(instance: number, options: PickOptions, token: CancellationToken): Promise { - const contents = new Promise((resolve, reject) => { + async $show(instance: number, options: TransferQuickPickOptions, token: CancellationToken): Promise { + const contents = new Promise((resolve, reject) => { this.items[instance] = { resolve, reject }; }); - options = { + const activeItem = await options.activeItem; + const transformedOptions = { ...options, onDidFocus: (el: any) => { if (el) { - this.proxy.$onItemSelected((el).handle); + this.proxy.$onItemSelected(Number.parseInt((el).id!)); } - } + }, + activeItem: this.isItem(activeItem) ? this.toQuickPickItem(activeItem) : undefined }; - const result = await this.delegate.pick(contents, options, token); + const result = await this.delegate.pick(contents, transformedOptions, token); if (Array.isArray(result)) { - return result.map(({ handle }) => handle); + return result.map(({ id }) => Number.parseInt(id!)); } else if (result) { - return result.handle; + return Number.parseInt(result.id!); } return undefined; } - $setItems(instance: number, items: TransferQuickPickItems[]): Promise { + private isItem(item?: TransferQuickPickItem): item is TransferQuickPickItem & { kind: 'item' } { + return item?.kind === 'item'; + } + + private toIconClasses(path: { light: string; dark: string } | ThemeIcon | string | undefined): string[] { + const iconClasses: string[] = []; + if (ThemeIcon.isThemeIcon(path)) { + const codicon = codiconArray(path.id); + iconClasses.push(...codicon); + } else if (path) { + const iconReference = this.sharedStyle.toIconClass(path); + this.toDispose.push(iconReference); + iconClasses.push(iconReference.object.iconClass); + } + return iconClasses; + } + + private toIconClass(path: { light: string; dark: string } | ThemeIcon | string | undefined): string { + return this.toIconClasses(path).join(' '); + } + + private toQuickPickItem(item: undefined): undefined; + private toQuickPickItem(item: TransferQuickPickItem & { kind: 'item' }): QuickPickItem; + private toQuickPickItem(item: TransferQuickPickItem & { kind: 'separator' }): QuickPickSeparator; + private toQuickPickItem(item: TransferQuickPickItem): QuickPickItemOrSeparator; + private toQuickPickItem(item: TransferQuickPickItem | undefined): QuickPickItemOrSeparator | undefined { + if (!item) { + return undefined; + } else if (item.kind === 'separator') { + return { + type: 'separator', + label: item.label + }; + } + return { + type: 'item', + id: item.handle.toString(), + label: item.label, + description: item.description, + detail: item.detail, + alwaysShow: item.alwaysShow, + iconClasses: this.toIconClasses(item.iconUrl), + buttons: item.buttons ? this.convertToQuickInputButtons(item.buttons) : undefined + }; + } + + $setItems(instance: number, items: TransferQuickPickItem[]): Promise { if (this.items[instance]) { - this.items[instance].resolve(items); + this.items[instance].resolve(items.map(item => this.toQuickPickItem(item))); delete this.items[instance]; } return Promise.resolve(); @@ -201,17 +253,17 @@ export class QuickOpenMainImpl implements QuickOpenMain, Disposable { quickPick.onDidAccept(() => { this.proxy.$acceptOnDidAccept(sessionId); }); - quickPick.onDidChangeActive((items: Array) => { - this.proxy.$onDidChangeActive(sessionId, items.map(item => (item as TransferQuickPickItems).handle)); + quickPick.onDidChangeActive((items: QuickPickItem[]) => { + this.proxy.$onDidChangeActive(sessionId, items.map(item => Number.parseInt(item.id!))); }); - quickPick.onDidChangeSelection((items: Array) => { - this.proxy.$onDidChangeSelection(sessionId, items.map(item => (item as TransferQuickPickItems).handle)); + quickPick.onDidChangeSelection((items: QuickPickItem[]) => { + this.proxy.$onDidChangeSelection(sessionId, items.map(item => Number.parseInt(item.id!))); }); quickPick.onDidTriggerButton((button: QuickInputButtonHandle) => { this.proxy.$acceptOnDidTriggerButton(sessionId, button); }); quickPick.onDidTriggerItemButton(e => { - this.proxy.$onDidTriggerItemButton(sessionId, (e.item as TransferQuickPickItems).handle, (e.button as TransferQuickPickItems).handle); + this.proxy.$onDidTriggerItemButton(sessionId, Number.parseInt(e.item.id!), (e.button as TransferQuickPickItem).handle); }); quickPick.onDidChangeValue((value: string) => { this.proxy.$acceptDidChangeValue(sessionId, value); @@ -260,10 +312,13 @@ export class QuickOpenMainImpl implements QuickOpenMain, Disposable { } } else if (param === 'items') { handlesToItems.clear(); - params[param].forEach((item: TransferQuickPickItems) => { - handlesToItems.set(item.handle, item); + const items: QuickPickItemOrSeparator[] = []; + params[param].forEach((transferItem: TransferQuickPickItem) => { + const item = this.toQuickPickItem(transferItem); + items.push(item); + handlesToItems.set(transferItem.handle, item); }); - (input as any)[param] = params[param]; + (input as any)[param] = items; } else if (param === 'activeItems' || param === 'selectedItems') { (input as any)[param] = params[param] .filter((handle: number) => handlesToItems.has(handle)) @@ -273,24 +328,12 @@ export class QuickOpenMainImpl implements QuickOpenMain, Disposable { if (button.handle === -1) { return this.quickInputService.backButton; } - const { iconPath, tooltip, handle } = button; - if ('id' in iconPath) { - return { - iconClass: ThemeIcon.asClassName(iconPath), - tooltip, - handle - }; - } else { - const monacoIconPath = (iconPath as unknown as { light: monaco.Uri, dark: monaco.Uri }); - return { - iconPath: { - dark: monaco.Uri.revive(monacoIconPath.dark), - light: monacoIconPath.light && monaco.Uri.revive(monacoIconPath.light) - }, - tooltip, - handle - }; - } + const { iconUrl, tooltip, handle } = button; + return { + tooltip, + handle, + iconClass: this.toIconClass(iconUrl) + }; }); } else { (input as any)[param] = params[param]; @@ -314,9 +357,9 @@ export class QuickOpenMainImpl implements QuickOpenMain, Disposable { return Promise.resolve(undefined); } - private convertToQuickInputButtons(buttons: Array): Array { + private convertToQuickInputButtons(buttons: readonly TransferQuickInputButton[]): QuickInputButton[] { return buttons.map((button, i) => ({ - ...getIconPathOrClass(button), + iconClass: this.toIconClass(button.iconUrl), tooltip: button.tooltip, handle: button === QuickInputButtons.Back ? -1 : i, } as QuickInputButton)); diff --git a/packages/plugin-ext/src/main/browser/scm-main.ts b/packages/plugin-ext/src/main/browser/scm-main.ts index 68af564e04111..ade01b4bc59d4 100644 --- a/packages/plugin-ext/src/main/browser/scm-main.ts +++ b/packages/plugin-ext/src/main/browser/scm-main.ts @@ -41,8 +41,8 @@ import { Splice } from '../../common/arrays'; import { UriComponents } from '../../common/uri-components'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { PluginSharedStyle } from './plugin-shared-style'; -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; import { IconUrl } from '../../common'; +import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; export class PluginScmResourceGroup implements ScmResourceGroup { diff --git a/packages/plugin-ext/src/main/browser/selection-provider-command.ts b/packages/plugin-ext/src/main/browser/selection-provider-command.ts index 081ce28490d8c..700e6e577cc30 100644 --- a/packages/plugin-ext/src/main/browser/selection-provider-command.ts +++ b/packages/plugin-ext/src/main/browser/selection-provider-command.ts @@ -19,7 +19,6 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { UriAwareCommandHandler, UriCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import URI from '@theia/core/lib/common/uri'; import { SelectionService } from '@theia/core'; -import { theiaUritoUriComponents } from '../../common/uri-components'; export namespace SelectionProviderCommands { export const GET_SELECTED_CONTEXT: Command = { @@ -36,7 +35,7 @@ export class SelectionProviderCommandContribution implements CommandContribution commands.registerCommand(SelectionProviderCommands.GET_SELECTED_CONTEXT, this.newMultiUriAwareCommandHandler({ isEnabled: () => true, isVisible: () => false, - execute: (selectedUris: URI[]) => selectedUris.map(uri => theiaUritoUriComponents(uri)) + execute: (selectedUris: URI[]) => selectedUris.map(uri => uri.toComponents()) })); } diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 5375409373fb8..d78631ef74d0c 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -24,6 +24,8 @@ import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor'; import { toUriComponents } from '../hierarchy/hierarchy-types-converters'; import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { DisposableCollection } from '@theia/core'; +import { NotebookEditorWidget } from '@theia/notebook/lib/browser'; +import { Deferred } from '@theia/core/lib/common/promise-util'; interface TabInfo { tab: TabDto; @@ -36,17 +38,25 @@ export class TabsMainImpl implements TabsMain, Disposable { private readonly proxy: TabsExt; private tabGroupModel = new Map, TabGroupDto>(); private tabInfoLookup = new Map, TabInfo>(); + private waitQueue = new Map(); private applicationShell: ApplicationShell; private disposableTabBarListeners: DisposableCollection = new DisposableCollection(); private toDisposeOnDestroy: DisposableCollection = new DisposableCollection(); - private groupIdCounter = 0; + private groupIdCounter = 1; private currentActiveGroup: TabGroupDto; private tabGroupChanged: boolean = false; + private readonly defaultTabGroup: TabGroupDto = { + groupId: 0, + tabs: [], + isActive: true, + viewColumn: 0 + }; + constructor( rpc: RPCProtocol, container: interfaces.Container @@ -57,7 +67,7 @@ export class TabsMainImpl implements TabsMain, Disposable { this.createTabsModel(); const tabBars = this.applicationShell.mainPanel.tabBars(); - for (let tabBar; tabBar = tabBars.next();) { + for (let tabBar: TabBar | undefined; tabBar = tabBars.next();) { this.attachListenersToTabBar(tabBar); } @@ -99,37 +109,65 @@ export class TabsMainImpl implements TabsMain, Disposable { }); } + waitForWidget(widget: Widget): Promise { + const deferred = new Deferred(); + this.waitQueue.set(widget, deferred); + + const timeout = setTimeout(() => { + deferred.resolve(); // resolve to unblock the event + }, 1000); + + deferred.promise.then(() => { + clearTimeout(timeout); + }); + + return deferred.promise; + } + protected createTabsModel(): void { + if (this.applicationShell.mainAreaTabBars.length === 0) { + this.proxy.$acceptEditorTabModel([this.defaultTabGroup]); + return; + } const newTabGroupModel = new Map, TabGroupDto>(); this.tabInfoLookup.clear(); this.disposableTabBarListeners.dispose(); - this.applicationShell.mainAreaTabBars.forEach(tabBar => { - this.attachListenersToTabBar(tabBar); - const groupDto = this.createTabGroupDto(tabBar); - tabBar.titles.forEach((title, index) => this.tabInfoLookup.set(title, { group: groupDto, tab: groupDto.tabs[index], tabIndex: index })); - newTabGroupModel.set(tabBar, groupDto); - }); + this.applicationShell.mainAreaTabBars + .forEach(tabBar => { + this.attachListenersToTabBar(tabBar); + const groupDto = this.createTabGroupDto(tabBar); + tabBar.titles.forEach((title, index) => this.tabInfoLookup.set(title, { group: groupDto, tab: groupDto.tabs[index], tabIndex: index })); + newTabGroupModel.set(tabBar, groupDto); + }); if (newTabGroupModel.size > 0 && Array.from(newTabGroupModel.values()).indexOf(this.currentActiveGroup) < 0) { this.currentActiveGroup = this.tabInfoLookup.get(this.applicationShell.mainPanel.currentTitle!)?.group ?? newTabGroupModel.values().next().value; this.currentActiveGroup.isActive = true; } this.tabGroupModel = newTabGroupModel; this.proxy.$acceptEditorTabModel(Array.from(this.tabGroupModel.values())); + // Resolve all waiting widget promises + this.waitQueue.forEach(deferred => deferred.resolve()); + this.waitQueue.clear(); } - protected createTabDto(tabTitle: Title, groupId: number): TabDto { + protected createTabDto(tabTitle: Title, groupId: number, newTab = false): TabDto { const widget = tabTitle.owner; + const active = newTab || this.getTabBar(tabTitle)?.currentTitle === tabTitle; return { id: this.createTabId(tabTitle, groupId), label: tabTitle.label, input: this.evaluateTabDtoInput(widget), - isActive: tabTitle.owner.isVisible, + isActive: active, isPinned: tabTitle.className.includes(PINNED_CLASS), isDirty: Saveable.isDirty(widget), isPreview: widget instanceof EditorPreviewWidget && widget.isPreview }; } + protected getTabBar(tabTitle: Title): TabBar | undefined { + return this.applicationShell.mainPanel.findTabBar(tabTitle); + } + protected createTabId(tabTitle: Title, groupId: number): string { return `${groupId}~${tabTitle.owner.id}`; } @@ -138,11 +176,12 @@ export class TabsMainImpl implements TabsMain, Disposable { const oldDto = this.tabGroupModel.get(tabBar); const groupId = oldDto?.groupId ?? this.groupIdCounter++; const tabs = tabBar.titles.map(title => this.createTabDto(title, groupId)); + const viewColumn = 0; // TODO: Implement correct viewColumn handling return { groupId, tabs, isActive: false, - viewColumn: 1 + viewColumn }; } @@ -172,7 +211,6 @@ export class TabsMainImpl implements TabsMain, Disposable { uri: toUriComponents(widget.editor.uri.toString()) }; } - // TODO notebook support when implemented } else if (widget instanceof ViewContainer) { return { kind: TabInputKind.WebviewEditorInput, @@ -182,14 +220,24 @@ export class TabsMainImpl implements TabsMain, Disposable { return { kind: TabInputKind.TerminalEditorInput }; + } else if (widget instanceof NotebookEditorWidget) { + return { + kind: TabInputKind.NotebookInput, + notebookType: widget.notebookType, + uri: toUriComponents(widget.model?.uri.toString() ?? '') + }; } return { kind: TabInputKind.UnknownInput }; } - protected connectToSignal(disposableList: DisposableCollection, signal: { connect(listener: T, context: unknown): void, disconnect(listener: T): void }, listener: T): void { + protected connectToSignal(disposableList: DisposableCollection, + signal: { + connect(listener: T, context: unknown): void, + disconnect(listener: T, context: unknown): void + }, listener: T): void { signal.connect(listener, this); - disposableList.push(Disposable.create(() => signal.disconnect(listener))); + disposableList.push(Disposable.create(() => signal.disconnect(listener, this))); } protected tabDtosEqual(a: TabDto, b: TabDto): boolean { @@ -214,7 +262,7 @@ export class TabsMainImpl implements TabsMain, Disposable { private onTabCreated(tabBar: TabBar, args: TabBar.ITabActivateRequestedArgs): void { const group = this.getOrRebuildModel(this.tabGroupModel, tabBar); this.connectToSignal(this.disposableTabBarListeners, args.title.changed, this.onTabTitleChanged); - const tabDto = this.createTabDto(args.title, group.groupId); + const tabDto = this.createTabDto(args.title, group.groupId, true); this.tabInfoLookup.set(args.title, { group, tab: tabDto, tabIndex: args.index }); group.tabs.splice(args.index, 0, tabDto); this.proxy.$acceptTabOperation({ @@ -223,6 +271,8 @@ export class TabsMainImpl implements TabsMain, Disposable { tabDto, groupId: group.groupId }); + this.waitQueue.get(args.title.owner)?.resolve(); + this.waitQueue.delete(args.title.owner); } private onTabTitleChanged(title: Title): void { @@ -232,6 +282,9 @@ export class TabsMainImpl implements TabsMain, Disposable { } const oldTabDto = tabInfo.tab; const newTabDto = this.createTabDto(title, tabInfo.group.groupId); + if (!oldTabDto.isActive && newTabDto.isActive) { + this.currentActiveGroup.tabs.filter(tab => tab.isActive).forEach(tab => tab.isActive = false); + } if (newTabDto.isActive && !tabInfo.group.isActive) { tabInfo.group.isActive = true; this.currentActiveGroup.isActive = false; @@ -248,6 +301,8 @@ export class TabsMainImpl implements TabsMain, Disposable { groupId: tabInfo.group.groupId }); } + this.waitQueue.get(title.owner)?.resolve(); + this.waitQueue.delete(title.owner); } private onTabClosed(tabInfo: TabInfo, title: Title): void { diff --git a/packages/plugin-ext/src/main/browser/terminal-main.ts b/packages/plugin-ext/src/main/browser/terminal-main.ts index fe1876c7bf8b4..2ac7779f8cee5 100644 --- a/packages/plugin-ext/src/main/browser/terminal-main.ts +++ b/packages/plugin-ext/src/main/browser/terminal-main.ts @@ -15,21 +15,29 @@ // ***************************************************************************** import { interfaces } from '@theia/core/shared/inversify'; -import { ApplicationShell, WidgetOpenerOptions } from '@theia/core/lib/browser'; -import { TerminalEditorLocationOptions, TerminalOptions } from '@theia/plugin'; +import { ApplicationShell, WidgetOpenerOptions, codicon } from '@theia/core/lib/browser'; +import { TerminalEditorLocationOptions } from '@theia/plugin'; import { TerminalLocation, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { TerminalProfileService } from '@theia/terminal/lib/browser/terminal-profile-service'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; -import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc'; +import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT, TerminalOptions } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { SerializableEnvironmentVariableCollection, ShellTerminalServerProxy } from '@theia/terminal/lib/common/shell-terminal-protocol'; import { TerminalLink, TerminalLinkProvider } from '@theia/terminal/lib/browser/terminal-link-provider'; import { URI } from '@theia/core/lib/common/uri'; -import { getIconClass } from '../../plugin/terminal-ext'; import { PluginTerminalRegistry } from './plugin-terminal-registry'; -import { CancellationToken } from '@theia/core'; +import { CancellationToken, isObject } from '@theia/core'; import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; +import { PluginSharedStyle } from './plugin-shared-style'; +import { ThemeIcon } from '@theia/core/lib/common/theme'; +import debounce = require('@theia/core/shared/lodash.debounce'); + +interface TerminalObserverData { + nrOfLinesToMatch: number; + outputMatcherRegex: RegExp + disposables: DisposableCollection; +} /** * Plugin api service allows working with terminal emulator. @@ -42,16 +50,19 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin private readonly hostedPluginSupport: HostedPluginSupport; private readonly shell: ApplicationShell; private readonly extProxy: TerminalServiceExt; + private readonly sharedStyle: PluginSharedStyle; private readonly shellTerminalServer: ShellTerminalServerProxy; private readonly terminalLinkProviders: string[] = []; private readonly toDispose = new DisposableCollection(); + private readonly observers = new Map(); constructor(rpc: RPCProtocol, container: interfaces.Container) { this.terminals = container.get(TerminalService); this.terminalProfileService = container.get(TerminalProfileService); this.pluginTerminalRegistry = container.get(PluginTerminalRegistry); this.hostedPluginSupport = container.get(HostedPluginSupport); + this.sharedStyle = container.get(PluginSharedStyle); this.shell = container.get(ApplicationShell); this.shellTerminalServer = container.get(ShellTerminalServerProxy); this.extProxy = rpc.getProxy(MAIN_RPC_CONTEXT.TERMINAL_EXT); @@ -121,6 +132,8 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin this.extProxy.$terminalOnInput(terminal.id, data); this.extProxy.$terminalStateChanged(terminal.id); })); + + this.observers.forEach((observer, id) => this.observeTerminal(id, terminal, observer)); } $write(id: string, data: string): void { @@ -143,7 +156,7 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin const terminal = await this.terminals.newTerminal({ id, title: options.name, - iconClass: getIconClass(options), + iconClass: this.toIconClass(options), shellPath: options.shellPath, shellArgs: options.shellArgs, cwd: options.cwd ? new URI(options.cwd) : undefined, @@ -181,11 +194,11 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin return undefined; } - $sendText(id: string, text: string, addNewLine?: boolean): void { + $sendText(id: string, text: string, shouldExecute?: boolean): void { const terminal = this.terminals.getById(id); if (terminal) { text = text.replace(/\r?\n/g, '\r'); - if (addNewLine && text.charAt(text.length - 1) !== '\r') { + if (shouldExecute && text.charAt(text.length - 1) !== '\r') { text += '\r'; } terminal.sendText(text); @@ -293,6 +306,59 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin } } + $registerTerminalObserver(id: string, nrOfLinesToMatch: number, outputMatcherRegex: string): void { + const observerData = { + nrOfLinesToMatch: nrOfLinesToMatch, + outputMatcherRegex: new RegExp(outputMatcherRegex, 'm'), + disposables: new DisposableCollection() + }; + this.observers.set(id, observerData); + this.terminals.all.forEach(terminal => { + this.observeTerminal(id, terminal, observerData); + }); + } + + protected observeTerminal(observerId: string, terminal: TerminalWidget, observerData: TerminalObserverData): void { + const doMatch = debounce(() => { + const lineCount = Math.min(observerData.nrOfLinesToMatch, terminal.buffer.length); + const lines = terminal.buffer.getLines(terminal.buffer.length - lineCount, lineCount); + const result = lines.join('\n').match(observerData.outputMatcherRegex); + if (result) { + this.extProxy.$reportOutputMatch(observerId, result.map(value => value)); + } + }); + observerData.disposables.push(terminal.onOutput(output => { + doMatch(); + })); + } + + protected toIconClass(options: TerminalOptions): string | ThemeIcon | undefined { + const iconColor = isObject<{ id: string }>(options.color) && typeof options.color.id === 'string' ? options.color.id : undefined; + let iconClass: string; + if (options.iconUrl) { + if (typeof options.iconUrl === 'object' && 'id' in options.iconUrl) { + iconClass = codicon(options.iconUrl.id); + } else { + const iconReference = this.sharedStyle.toIconClass(options.iconUrl); + this.toDispose.push(iconReference); + iconClass = iconReference.object.iconClass; + } + } else { + iconClass = codicon('terminal'); + } + return iconColor ? { id: iconClass, color: { id: iconColor } } : iconClass; + } + + $unregisterTerminalObserver(id: string): void { + const observer = this.observers.get(id); + if (observer) { + observer.disposables.dispose(); + this.observers.delete(id); + } else { + throw new Error(`Unregistering unknown terminal observer: ${id}`); + } + } + async provideLinks(line: string, terminal: TerminalWidget, cancellationToken?: CancellationToken | undefined): Promise { if (this.terminalLinkProviders.length < 1) { return []; diff --git a/packages/plugin-ext/src/main/browser/test-main.ts b/packages/plugin-ext/src/main/browser/test-main.ts index e6aa75a80a86d..3662373142758 100644 --- a/packages/plugin-ext/src/main/browser/test-main.ts +++ b/packages/plugin-ext/src/main/browser/test-main.ts @@ -18,6 +18,7 @@ import { SimpleObservableCollection, TreeCollection, observableProperty } from ' import { TestController, TestItem, TestOutputItem, TestRun, TestRunProfile, TestService, TestState, TestStateChangedEvent } from '@theia/test/lib/browser/test-service'; +import { TestExecutionProgressService } from '@theia/test/lib/browser/test-execution-progress-service'; import { AccumulatingTreeDeltaEmitter, CollectionDelta, DeltaKind, TreeDelta, TreeDeltaBuilder } from '@theia/test/lib/common/tree-delta'; import { Emitter, Location, Range } from '@theia/core/shared/vscode-languageserver-protocol'; import { Range as PluginRange, Location as PluginLocation } from '../../common/plugin-api-rpc-model'; @@ -26,7 +27,10 @@ import { CancellationToken, Disposable, Event, URI } from '@theia/core'; import { MAIN_RPC_CONTEXT, TestControllerUpdate, TestingExt, TestingMain } from '../../common'; import { RPCProtocol } from '../../common/rpc-protocol'; import { interfaces } from '@theia/core/shared/inversify'; -import { TestExecutionState, TestItemDTO, TestItemReference, TestOutputDTO, TestRunDTO, TestRunProfileDTO, TestStateChangeDTO } from '../../common/test-types'; +import { + TestExecutionState, TestItemDTO, TestItemReference, TestOutputDTO, + TestRunDTO, TestRunProfileDTO, TestStateChangeDTO +} from '../../common/test-types'; import { TestRunProfileKind } from '../../plugin/types-impl'; import { CommandRegistryMainImpl } from './command-registry-main'; @@ -195,7 +199,16 @@ function itemToPath(item: TestItem): string[] { class TestRunProfileImpl implements TestRunProfile { label: string; - isDefault: boolean; + + private _isDefault: boolean; + set isDefault(isDefault: boolean) { + this._isDefault = isDefault; + this.proxy.$onDidChangeDefault(this.controllerId, this.id, isDefault); + } + get isDefault(): boolean { + return this._isDefault; + } + tag: string; canConfigure: boolean; @@ -205,7 +218,7 @@ class TestRunProfileImpl implements TestRunProfile { } if ('isDefault' in update) { - this.isDefault = update.isDefault!; + this._isDefault = update.isDefault!; } if ('tag' in update) { @@ -236,13 +249,14 @@ class TestRunProfileImpl implements TestRunProfile { this.proxy.$onConfigureRunProfile(this.controllerId, this.id); } - run(name: string, included: TestItem[], excluded: TestItem[]): void { + run(name: string, included: TestItem[], excluded: TestItem[], preserveFocus: boolean): void { this.proxy.$onRunControllerTests([{ controllerId: this.controllerId, name, profileId: this.id, includedTests: included.map(item => itemToPath(item)), - excludedTests: excluded.map(item => itemToPath(item)) + excludedTests: excluded.map(item => itemToPath(item)), + preserveFocus }]); } } @@ -542,12 +556,14 @@ class TestControllerImpl implements TestController { export class TestingMainImpl implements TestingMain { private testService: TestService; + private testExecutionProgressService: TestExecutionProgressService; private controllerRegistrations = new Map(); private proxy: TestingExt; canRefresh: boolean; constructor(rpc: RPCProtocol, container: interfaces.Container, commandRegistry: CommandRegistryMainImpl) { this.testService = container.get(TestService); + this.testExecutionProgressService = container.get(TestExecutionProgressService); this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TESTING_EXT); commandRegistry.registerArgumentProcessor({ // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -605,7 +621,8 @@ export class TestingMainImpl implements TestingMain { $removeTestRunProfile(controllerId: string, profileId: string): void { this.withController(controllerId).removeProfile(profileId); } - $notifyTestRunCreated(controllerId: string, run: TestRunDTO): void { + $notifyTestRunCreated(controllerId: string, run: TestRunDTO, preserveFocus: boolean): void { + this.testExecutionProgressService.onTestRunRequested(preserveFocus); this.withController(controllerId).addRun(run.id, run.name, run.isRunning); } $notifyTestStateChanged(controllerId: string, runId: string, stateChanges: TestStateChangeDTO[], outputChanges: TestOutputDTO[]): void { diff --git a/packages/plugin-ext/src/main/browser/text-editor-main.ts b/packages/plugin-ext/src/main/browser/text-editor-main.ts index 17656f9a85d2e..f71d93a18d3fe 100644 --- a/packages/plugin-ext/src/main/browser/text-editor-main.ts +++ b/packages/plugin-ext/src/main/browser/text-editor-main.ts @@ -33,11 +33,14 @@ import { Range } from '../../common/plugin-api-rpc-model'; import { Emitter, Event } from '@theia/core'; import { TextEditorCursorStyle, cursorStyleToString } from '../../common/editor-options'; import { TextEditorLineNumbersStyle, EndOfLine } from '../../plugin/types-impl'; +import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor'; +import { EndOfLineSequence, ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model'; +import { EditorOption, RenderLineNumbersType } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions'; export class TextEditorMain implements Disposable { private properties: TextEditorPropertiesMain | undefined; - private editor: MonacoEditor | undefined; + private editor: MonacoEditor | SimpleMonacoEditor | undefined; private readonly onPropertiesChangedEmitter = new Emitter(); @@ -48,8 +51,8 @@ export class TextEditorMain implements Disposable { constructor( private id: string, - private model: monaco.editor.IModel, - editor: MonacoEditor + private model: monaco.editor.IModel | ITextModel, + editor: MonacoEditor | SimpleMonacoEditor ) { this.toDispose.push(this.model.onDidChangeOptions(() => this.updateProperties(undefined) @@ -76,7 +79,7 @@ export class TextEditorMain implements Disposable { protected readonly toDisposeOnEditor = new DisposableCollection(); - private setEditor(editor?: MonacoEditor): void { + private setEditor(editor?: MonacoEditor | SimpleMonacoEditor): void { if (this.editor === editor) { return; } @@ -115,7 +118,7 @@ export class TextEditorMain implements Disposable { return this.id; } - getModel(): monaco.editor.IModel { + getModel(): monaco.editor.IModel | ITextModel { return this.model; } @@ -151,7 +154,7 @@ export class TextEditorMain implements Disposable { } if (typeof newConfiguration.lineNumbers !== 'undefined') { - let lineNumbers: 'on' | 'off' | 'relative'; + let lineNumbers: 'on' | 'off' | 'relative' | 'interval'; switch (newConfiguration.lineNumbers) { case TextEditorLineNumbersStyle.On: lineNumbers = 'on'; @@ -159,6 +162,9 @@ export class TextEditorMain implements Disposable { case TextEditorLineNumbersStyle.Relative: lineNumbers = 'relative'; break; + case TextEditorLineNumbersStyle.Interval: + lineNumbers = 'interval'; + break; default: lineNumbers = 'off'; } @@ -205,7 +211,7 @@ export class TextEditorMain implements Disposable { } revealRange(range: monaco.Range, revealType: TextEditorRevealType): void { - if (!this.editor) { + if (!this.editor || this.editor instanceof SimpleMonacoEditor) { return; } switch (revealType) { @@ -237,10 +243,14 @@ export class TextEditorMain implements Disposable { return false; } - if (opts.setEndOfLine === EndOfLine.CRLF) { + if (opts.setEndOfLine === EndOfLine.CRLF && !this.isSimpleWidget(this.model)) { this.model.setEOL(monaco.editor.EndOfLineSequence.CRLF); - } else if (opts.setEndOfLine === EndOfLine.LF) { + } else if (opts.setEndOfLine === EndOfLine.LF && !this.isSimpleWidget(this.model)) { this.model.setEOL(monaco.editor.EndOfLineSequence.LF); + } else if (opts.setEndOfLine === EndOfLine.CRLF && this.isSimpleWidget(this.model)) { + this.model.setEOL(EndOfLineSequence.CRLF); + } else if (opts.setEndOfLine === EndOfLine.LF && this.isSimpleWidget(this.model)) { + this.model.setEOL(EndOfLineSequence.CRLF); } const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = []; @@ -308,6 +318,10 @@ export class TextEditorMain implements Disposable { private static toMonacoSelections(selection: Selection): monaco.Selection { return new monaco.Selection(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn); } + + private isSimpleWidget(model: monaco.editor.IModel | ITextModel): model is ITextModel { + return !!(model as ITextModel).isForSimpleWidget; + } } // TODO move to monaco typings! @@ -359,17 +373,26 @@ export class TextEditorPropertiesMain { return undefined; } - static readFromEditor(prevProperties: TextEditorPropertiesMain | undefined, model: monaco.editor.IModel, editor: MonacoEditor): TextEditorPropertiesMain { + static readFromEditor(prevProperties: TextEditorPropertiesMain | undefined, + model: monaco.editor.IModel | ITextModel, + editor: MonacoEditor | SimpleMonacoEditor): TextEditorPropertiesMain { + const selections = TextEditorPropertiesMain.getSelectionsFromEditor(prevProperties, editor); const options = TextEditorPropertiesMain.getOptionsFromEditor(prevProperties, model, editor); const visibleRanges = TextEditorPropertiesMain.getVisibleRangesFromEditor(prevProperties, editor); return new TextEditorPropertiesMain(selections, options, visibleRanges); } - private static getSelectionsFromEditor(prevProperties: TextEditorPropertiesMain | undefined, editor: MonacoEditor): monaco.Selection[] { + private static getSelectionsFromEditor(prevProperties: TextEditorPropertiesMain | undefined, editor: MonacoEditor | SimpleMonacoEditor): monaco.Selection[] { let result: monaco.Selection[] | undefined = undefined; - if (editor) { + if (editor && editor instanceof MonacoEditor) { result = editor.getControl().getSelections() || undefined; + } else if (editor && editor instanceof SimpleMonacoEditor) { + result = editor.getControl().getSelections()?.map(selection => new monaco.Selection( + selection.startLineNumber, + selection.startColumn, + selection.positionLineNumber, + selection.positionColumn)); } if (!result && prevProperties) { @@ -382,14 +405,16 @@ export class TextEditorPropertiesMain { return result; } - private static getOptionsFromEditor(prevProperties: TextEditorPropertiesMain | undefined, model: monaco.editor.IModel, editor: MonacoEditor): TextEditorConfiguration { + private static getOptionsFromEditor(prevProperties: TextEditorPropertiesMain | undefined, + model: monaco.editor.IModel | ITextModel, + editor: MonacoEditor | SimpleMonacoEditor): TextEditorConfiguration { if (model.isDisposed()) { return prevProperties!.options; } let cursorStyle: TextEditorCursorStyle; let lineNumbers: TextEditorLineNumbersStyle; - if (editor) { + if (editor && editor instanceof MonacoEditor) { const editorOptions = editor.getControl().getOptions(); const lineNumbersOpts = editorOptions.get(monaco.editor.EditorOption.lineNumbers); cursorStyle = editorOptions.get(monaco.editor.EditorOption.cursorStyle); @@ -400,10 +425,32 @@ export class TextEditorPropertiesMain { case monaco.editor.RenderLineNumbersType.Relative: lineNumbers = TextEditorLineNumbersStyle.Relative; break; + case monaco.editor.RenderLineNumbersType.Interval: + lineNumbers = TextEditorLineNumbersStyle.Interval; + break; + default: + lineNumbers = TextEditorLineNumbersStyle.On; + break; + } + } else if (editor && editor instanceof SimpleMonacoEditor) { + const editorOptions = editor.getControl().getOptions(); + const lineNumbersOpts = editorOptions.get(EditorOption.lineNumbers); + cursorStyle = editorOptions.get(EditorOption.cursorStyle); + switch (lineNumbersOpts.renderType) { + case RenderLineNumbersType.Off: + lineNumbers = TextEditorLineNumbersStyle.Off; + break; + case RenderLineNumbersType.Relative: + lineNumbers = TextEditorLineNumbersStyle.Relative; + break; + case RenderLineNumbersType.Interval: + lineNumbers = TextEditorLineNumbersStyle.Interval; + break; default: lineNumbers = TextEditorLineNumbersStyle.On; break; } + } else if (prevProperties) { cursorStyle = prevProperties.options.cursorStyle; lineNumbers = prevProperties.options.lineNumbers; @@ -422,7 +469,7 @@ export class TextEditorPropertiesMain { }; } - private static getVisibleRangesFromEditor(prevProperties: TextEditorPropertiesMain | undefined, editor: MonacoEditor): monaco.Range[] { + private static getVisibleRangesFromEditor(prevProperties: TextEditorPropertiesMain | undefined, editor: MonacoEditor | SimpleMonacoEditor): monaco.Range[] { if (editor) { return editor.getControl().getVisibleRanges(); } diff --git a/packages/plugin-ext/src/main/browser/text-editor-model-service.ts b/packages/plugin-ext/src/main/browser/text-editor-model-service.ts index 693aca0f4ba4b..5e03e16601571 100644 --- a/packages/plugin-ext/src/main/browser/text-editor-model-service.ts +++ b/packages/plugin-ext/src/main/browser/text-editor-model-service.ts @@ -76,6 +76,15 @@ export class EditorModelService { return this.monacoModelService.models; } + async save(uri: URI): Promise { + const model = this.monacoModelService.get(uri.toString()); + if (model) { + await model.save(); + return true; + } + return false; + } + async saveAll(includeUntitled?: boolean): Promise { const saves = []; for (const model of this.monacoModelService.models) { diff --git a/packages/plugin-ext/src/main/browser/text-editors-main.ts b/packages/plugin-ext/src/main/browser/text-editors-main.ts index 879a709a04c45..93c2d820c54ef 100644 --- a/packages/plugin-ext/src/main/browser/text-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/text-editors-main.ts @@ -14,7 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { URI } from '@theia/core/shared/vscode-uri'; import { TextEditorsMain, MAIN_RPC_CONTEXT, @@ -29,6 +28,7 @@ import { ThemeDecorationInstanceRenderOptions, DecorationOptions, WorkspaceEditDto, + WorkspaceNotebookCellEditDto, DocumentsMain, WorkspaceEditMetadataDto, } from '../../common/plugin-api-rpc'; @@ -40,12 +40,17 @@ import { TextEditorMain } from './text-editor-main'; import { disposed } from '../../common/errors'; import { toMonacoWorkspaceEdit } from './languages-main'; import { MonacoBulkEditService } from '@theia/monaco/lib/browser/monaco-bulk-edit-service'; -import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; -import { theiaUritoUriComponents, UriComponents } from '../../common/uri-components'; +import { UriComponents } from '../../common/uri-components'; import { Endpoint } from '@theia/core/lib/browser/endpoint'; import * as monaco from '@theia/monaco-editor-core'; import { ResourceEdit } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService'; import { IDecorationRenderOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/editorCommon'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; +import { ArrayUtils, URI } from '@theia/core'; +import { toNotebookWorspaceEdit } from './notebooks/notebooks-main'; +import { interfaces } from '@theia/core/shared/inversify'; +import { NotebookService } from '@theia/notebook/lib/browser'; export class TextEditorsMainImpl implements TextEditorsMain, Disposable { @@ -54,14 +59,20 @@ export class TextEditorsMainImpl implements TextEditorsMain, Disposable { private readonly editorsToDispose = new Map(); private readonly fileEndpoint = new Endpoint({ path: 'file' }).getRestUrl(); + private readonly bulkEditService: MonacoBulkEditService; + private readonly notebookService: NotebookService; + constructor( private readonly editorsAndDocuments: EditorsAndDocumentsMain, private readonly documents: DocumentsMain, rpc: RPCProtocol, - private readonly bulkEditService: MonacoBulkEditService, - private readonly monacoEditorService: MonacoEditorService, + container: interfaces.Container ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TEXT_EDITORS_EXT); + + this.bulkEditService = container.get(MonacoBulkEditService); + this.notebookService = container.get(NotebookService); + this.toDispose.push(editorsAndDocuments); this.toDispose.push(editorsAndDocuments.onTextEditorAdd(editors => editors.forEach(this.onTextEditorAdd, this))); this.toDispose.push(editorsAndDocuments.onTextEditorRemove(editors => editors.forEach(this.onTextEditorRemove, this))); @@ -128,11 +139,19 @@ export class TextEditorsMainImpl implements TextEditorsMain, Disposable { } async $tryApplyWorkspaceEdit(dto: WorkspaceEditDto, metadata?: WorkspaceEditMetadataDto): Promise { - const workspaceEdit = toMonacoWorkspaceEdit(dto); + const [notebookEdits, monacoEdits] = ArrayUtils.partition(dto.edits, edit => WorkspaceNotebookCellEditDto.is(edit)); try { - const edits = ResourceEdit.convert(workspaceEdit); - const { success } = await this.bulkEditService.apply(edits, { respectAutoSaveConfig: metadata?.isRefactoring }); - return success; + if (notebookEdits.length > 0) { + const workspaceEdit = toNotebookWorspaceEdit({ edits: notebookEdits }); + return this.notebookService.applyWorkspaceEdit(workspaceEdit); + } + if (monacoEdits.length > 0) { + const workspaceEdit = toMonacoWorkspaceEdit({ edits: monacoEdits }); + const edits = ResourceEdit.convert(workspaceEdit); + const { isApplied } = await this.bulkEditService.apply(edits, { respectAutoSaveConfig: metadata?.isRefactoring }); + return isApplied; + } + return false; } catch { return false; } @@ -145,9 +164,9 @@ export class TextEditorsMainImpl implements TextEditorsMain, Disposable { return Promise.resolve(this.editorsAndDocuments.getEditor(id)!.insertSnippet(template, ranges, opts)); } - $registerTextEditorDecorationType(key: string, options: DecorationRenderOptions | IDecorationRenderOptions): void { + $registerTextEditorDecorationType(key: string, options: DecorationRenderOptions): void { this.injectRemoteUris(options); - this.monacoEditorService.registerDecorationType('Plugin decoration', key, options as IDecorationRenderOptions); + StandaloneServices.get(ICodeEditorService).registerDecorationType('Plugin decoration', key, options as IDecorationRenderOptions); this.toDispose.push(Disposable.create(() => this.$removeTextEditorDecorationType(key))); } @@ -171,13 +190,13 @@ export class TextEditorsMainImpl implements TextEditorsMain, Disposable { protected toRemoteUri(uri?: UriComponents): UriComponents | undefined { if (uri && uri.scheme === 'file') { - return theiaUritoUriComponents(this.fileEndpoint.withQuery(URI.revive(uri).toString())); + return this.fileEndpoint.withQuery(URI.fromComponents(uri).toString()).toComponents(); } return uri; } $removeTextEditorDecorationType(key: string): void { - this.monacoEditorService.removeDecorationType(key); + StandaloneServices.get(ICodeEditorService).removeDecorationType(key); } $tryHideEditor(id: string): Promise { @@ -200,6 +219,14 @@ export class TextEditorsMainImpl implements TextEditorsMain, Disposable { return Promise.resolve(); } + $save(uri: UriComponents): PromiseLike { + return this.editorsAndDocuments.save(URI.fromComponents(uri)).then(u => u?.toComponents()); + } + + $saveAs(uri: UriComponents): PromiseLike { + return this.editorsAndDocuments.saveAs(URI.fromComponents(uri)).then(u => u?.toComponents()); + } + $saveAll(includeUntitled?: boolean): Promise { return this.editorsAndDocuments.saveAll(includeUntitled); } diff --git a/packages/plugin-ext/src/main/browser/theme-icon-override.ts b/packages/plugin-ext/src/main/browser/theme-icon-override.ts index 5ac10215ddb86..d22f101520463 100644 --- a/packages/plugin-ext/src/main/browser/theme-icon-override.ts +++ b/packages/plugin-ext/src/main/browser/theme-icon-override.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { getIconRegistry } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/iconRegistry'; // @monaco-uplift // Keep this up-to-date with the table at https://code.visualstudio.com/api/references/icons-in-labels#icon-listing @@ -27,11 +27,12 @@ const codeIconMap: Record = { 'callhierarchy-outgoing': 'call-outgoing', 'callstack-view-icon': 'debug-alt', 'callstack-view-session': 'bug', + 'chat-editor-label-icon': 'comment-discussion', 'comments-view-icon': 'comment-discussion', 'debug-breakpoint': 'debug-breakpoint', 'debug-breakpoint-conditional': 'debug-breakpoint-conditional', 'debug-breakpoint-conditional-disabled': 'debug-breakpoint-conditional-disabled', - 'debug-breakpoint-conditional-verified': 'debug-breakpoint-conditional-unverified', + 'debug-breakpoint-conditional-unverified': 'debug-breakpoint-conditional-unverified', 'debug-breakpoint-data': 'debug-breakpoint-data', 'debug-breakpoint-data-disabled': 'debug-breakpoint-data-disabled', 'debug-breakpoint-data-unverified': 'debug-breakpoint-data-unverified', @@ -173,9 +174,11 @@ const codeIconMap: Record = { 'remote-explorer-view-icon': 'remote-explorer', 'review-comment-collapse': 'chevron-up', 'run-view-icon': 'debug-alt', + 'runtime-extensions-editor-label-icon': ' extensions', 'search-clear-results': 'clear-all', 'search-collapse-results': 'collapse-all', 'search-details': 'ellipsis', + 'search-editor-label-icon': 'search', 'search-expand-results': 'expand-all', 'search-hide-replace': 'chevron-right', 'search-new-editor': 'new-file', @@ -190,6 +193,7 @@ const codeIconMap: Record = { 'settings-add': 'add', 'settings-discard': 'discard', 'settings-edit': 'edit', + 'settings-editor-label-icon': 'settings', 'settings-folder-dropdown': 'triangle-down', 'settings-group-collapsed': 'chevron-right', 'settings-group-expanded': 'chevron-down', @@ -229,24 +233,14 @@ const codeIconMap: Record = { 'watch-expressions-add-function-breakpoint': 'add', 'watch-expressions-remove-all': 'close-all', 'watch-view-icon': 'debug-alt', - 'widget-close': 'close' + 'widget-close': 'close', + 'workspace-trust-editor-label-icon': ' shield' }; -const originalAsCSSSelector = ThemeIcon.asCSSSelector; -const originalAsClassName = ThemeIcon.asClassName; -const originalAsClassNameArray = ThemeIcon.asClassNameArray; +const registry = getIconRegistry(); -function buildMappedIcon(icon: ThemeIcon): ThemeIcon { - const id = codeIconMap[icon.id] ?? icon.id; - const newIcon: ThemeIcon = { - ...icon, - id - }; - return newIcon; +for (const key in codeIconMap) { + if (codeIconMap.hasOwnProperty(key)) { + registry.registerIcon(key, { id: codeIconMap[key] }, key); + } } - -Object.assign(ThemeIcon, { - asCSSSelector: (icon: ThemeIcon) => originalAsCSSSelector(buildMappedIcon(icon)), - asClassName: (icon: ThemeIcon) => originalAsClassName(buildMappedIcon(icon)), - asClassNameArray: (icon: ThemeIcon) => originalAsClassNameArray(buildMappedIcon(icon)) -}); diff --git a/packages/plugin-ext/src/main/browser/uri-main.ts b/packages/plugin-ext/src/main/browser/uri-main.ts new file mode 100644 index 0000000000000..6e18cdea5241c --- /dev/null +++ b/packages/plugin-ext/src/main/browser/uri-main.ts @@ -0,0 +1,72 @@ +// ***************************************************************************** +// Copyright (C) 2024 STMicroelectronics. +// +// 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 { Disposable, URI } from '@theia/core'; +import { MAIN_RPC_CONTEXT, UriExt, UriMain } from '../../common'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { interfaces } from '@theia/core/shared/inversify'; +import { OpenHandler, OpenerOptions, OpenerService } from '@theia/core/lib/browser'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; + +export class UriMainImpl implements UriMain, Disposable { + private readonly proxy: UriExt; + private handlers = new Set(); + private readonly openerService: OpenerService; + private readonly pluginSupport: HostedPluginSupport; + private readonly openHandler: OpenHandler; + + constructor(rpc: RPCProtocol, container: interfaces.Container) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.URI_EXT); + this.openerService = container.get(OpenerService); + this.pluginSupport = container.get(HostedPluginSupport); + + this.openHandler = { + id: 'theia-plugin-open-handler', + canHandle: async (uri: URI, options?: OpenerOptions | undefined): Promise => { + if (uri.scheme !== FrontendApplicationConfigProvider.get().electron.uriScheme) { + return 0; + } + await this.pluginSupport.activateByUri(uri.scheme, uri.authority); + if (this.handlers.has(uri.authority)) { + return 500; + } + return 0; + }, + open: async (uri: URI, options?: OpenerOptions | undefined): Promise => { + if (!this.handlers.has(uri.authority)) { + throw new Error(`No plugin to handle this uri: : '${uri}'`); + } + this.proxy.$handleExternalUri(uri.toComponents()); + } + }; + + this.openerService.addHandler?.(this.openHandler); + } + + dispose(): void { + this.openerService.removeHandler?.(this.openHandler); + this.handlers.clear(); + } + + async $registerUriHandler(pluginId: string, extensionDisplayName: string): Promise { + this.handlers.add(pluginId); + } + + async $unregisterUriHandler(pluginId: string): Promise { + this.handlers.delete(pluginId); + } +} diff --git a/packages/plugin-ext/src/main/browser/view/plugin-tree-view-node-label-provider.ts b/packages/plugin-ext/src/main/browser/view/plugin-tree-view-node-label-provider.ts index 011ee0638e937..c5670f830cbe2 100644 --- a/packages/plugin-ext/src/main/browser/view/plugin-tree-view-node-label-provider.ts +++ b/packages/plugin-ext/src/main/browser/view/plugin-tree-view-node-label-provider.ts @@ -20,7 +20,7 @@ import { LabelProviderContribution, LabelProvider, URIIconReference } from '@the import { TreeLabelProvider } from '@theia/core/lib/browser/tree/tree-label-provider'; import { TreeViewNode } from './tree-view-widget'; import { TreeNode } from '@theia/core/lib/browser/tree/tree'; -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; @injectable() export class PluginTreeViewNodeLabelProvider implements LabelProviderContribution { diff --git a/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts b/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts index 27ec5950870a1..58dcd9457acd2 100644 --- a/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts +++ b/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts @@ -18,7 +18,7 @@ import { injectable, inject, postConstruct, optional } from '@theia/core/shared/ import { ApplicationShell, ViewContainer as ViewContainerWidget, WidgetManager, QuickViewService, ViewContainerIdentifier, ViewContainerTitleOptions, Widget, FrontendApplicationContribution, - StatefulWidget, CommonMenus, TreeViewWelcomeWidget, ViewContainerPart, BaseWidget + StatefulWidget, CommonMenus, TreeViewWelcomeWidget, ViewContainerPart, BaseWidget, } from '@theia/core/lib/browser'; import { ViewContainer, View, ViewWelcome, PluginViewType } from '../../../common'; import { PluginSharedStyle } from '../plugin-shared-style'; @@ -40,14 +40,14 @@ import { DebugConsoleContribution } from '@theia/debug/lib/browser/console/debug import { TreeViewWidget } from './tree-view-widget'; import { SEARCH_VIEW_CONTAINER_ID } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory'; import { TEST_VIEW_CONTAINER_ID } from '@theia/test/lib/browser/view/test-view-contribution'; -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; import { WebviewView, WebviewViewResolver } from '../webview-views/webview-views'; import { WebviewWidget, WebviewWidgetIdentifier } from '../webview/webview'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import { nls } from '@theia/core'; import { TheiaDockPanel } from '@theia/core/lib/browser/shell/theia-dock-panel'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; export const PLUGIN_VIEW_FACTORY_ID = 'plugin-view'; export const PLUGIN_VIEW_CONTAINER_FACTORY_ID = 'plugin-view-container'; @@ -55,6 +55,13 @@ export const PLUGIN_VIEW_DATA_FACTORY_ID = 'plugin-view-data'; export type ViewDataProvider = (params: { state?: object, viewInfo: View }) => Promise; +export interface ViewContainerInfo { + id: string + location: string + options: ViewContainerTitleOptions + onViewAdded: () => void +} + @injectable() export class PluginViewRegistry implements FrontendApplicationContribution { @@ -96,7 +103,7 @@ export class PluginViewRegistry implements FrontendApplicationContribution { private readonly views = new Map(); private readonly viewsWelcome = new Map(); - private readonly viewContainers = new Map(); + private readonly viewContainers = new Map(); private readonly containerViews = new Map(); private readonly viewClauseContexts = new Map | undefined>(); @@ -109,6 +116,16 @@ export class PluginViewRegistry implements FrontendApplicationContribution { private readonly webviewViewRevivals = new Map }>(); + private nextViewContainerId = 0; + + private static readonly BUILTIN_VIEW_CONTAINERS = new Set([ + 'explorer', + 'scm', + 'search', + 'test', + 'debug' + ]); + private static readonly ID_MAPPINGS: Map = new Map([ // VS Code Viewlets [EXPLORER_VIEW_CONTAINER_ID, 'workbench.view.explorer'], @@ -266,14 +283,15 @@ export class PluginViewRegistry implements FrontendApplicationContribution { } registerViewContainer(location: string, viewContainer: ViewContainer): Disposable { - if (this.viewContainers.has(viewContainer.id)) { + const containerId = `workbench.view.extension.${viewContainer.id}`; + if (this.viewContainers.has(containerId)) { console.warn('view container such id already registered: ', JSON.stringify(viewContainer)); return Disposable.NULL; } const toDispose = new DisposableCollection(); const containerClass = 'theia-plugin-view-container'; let themeIconClass = ''; - const iconClass = 'plugin-view-container-icon-' + viewContainer.id; + const iconClass = 'plugin-view-container-icon-' + this.nextViewContainerId++; // having dots in class would not work for css, so we need to generate an id. if (viewContainer.themeIcon) { const icon = ThemeIcon.fromString(viewContainer.themeIcon); @@ -290,7 +308,7 @@ export class PluginViewRegistry implements FrontendApplicationContribution { `)); } - toDispose.push(this.doRegisterViewContainer(viewContainer.id, location, { + toDispose.push(this.doRegisterViewContainer(containerId, location, { label: viewContainer.title, // The container class automatically sets a mask; if we're using a theme icon, we don't want one. iconClass: (themeIconClass || containerClass) + ' ' + iconClass, @@ -313,34 +331,47 @@ export class PluginViewRegistry implements FrontendApplicationContribution { protected doRegisterViewContainer(id: string, location: string, options: ViewContainerTitleOptions): Disposable { const toDispose = new DisposableCollection(); - this.viewContainers.set(id, [location, options]); toDispose.push(Disposable.create(() => this.viewContainers.delete(id))); const toggleCommandId = `plugin.view-container.${id}.toggle`; - toDispose.push(this.commands.registerCommand({ - id: toggleCommandId, - label: 'Toggle ' + options.label + ' View' - }, { - execute: () => this.toggleViewContainer(id) - })); - toDispose.push(this.menus.registerMenuAction(CommonMenus.VIEW_VIEWS, { - commandId: toggleCommandId, - label: options.label - })); - toDispose.push(this.quickView?.registerItem({ - label: options.label, - open: async () => { - const widget = await this.openViewContainer(id); + // Some plugins may register empty view containers. + // We should not register commands for them immediately, as that leads to bad UX. + // Instead, we register commands the first time we add a view to them. + let activate = () => { + toDispose.push(this.commands.registerCommand({ + id: toggleCommandId, + category: nls.localizeByDefault('View'), + label: nls.localizeByDefault('Toggle {0}', options.label) + }, { + execute: () => this.toggleViewContainer(id) + })); + toDispose.push(this.menus.registerMenuAction(CommonMenus.VIEW_VIEWS, { + commandId: toggleCommandId, + label: options.label + })); + toDispose.push(this.quickView?.registerItem({ + label: options.label, + open: async () => { + const widget = await this.openViewContainer(id); + if (widget) { + this.shell.activateWidget(widget.id); + } + } + })); + toDispose.push(Disposable.create(async () => { + const widget = await this.getPluginViewContainer(id); if (widget) { - this.shell.activateWidget(widget.id); + widget.dispose(); } - } - })); - toDispose.push(Disposable.create(async () => { - const widget = await this.getPluginViewContainer(id); - if (widget) { - widget.dispose(); - } - })); + })); + // Ignore every subsequent activation call + activate = () => { }; + }; + this.viewContainers.set(id, { + id, + location, + options, + onViewAdded: () => activate() + }); return toDispose; } @@ -349,6 +380,10 @@ export class PluginViewRegistry implements FrontendApplicationContribution { } registerView(viewContainerId: string, view: View): Disposable { + if (!PluginViewRegistry.BUILTIN_VIEW_CONTAINERS.has(viewContainerId)) { + // if it's not a built-in view container, it must be a contributed view container, see https://github.com/eclipse-theia/theia/issues/13249 + viewContainerId = `workbench.view.extension.${viewContainerId}`; + } if (this.views.has(view.id)) { console.warn('view with such id already registered: ', JSON.stringify(view)); return Disposable.NULL; @@ -359,6 +394,11 @@ export class PluginViewRegistry implements FrontendApplicationContribution { this.views.set(view.id, [viewContainerId, view]); toDispose.push(Disposable.create(() => this.views.delete(view.id))); + const containerInfo = this.viewContainers.get(viewContainerId); + if (containerInfo) { + containerInfo.onViewAdded(); + } + const containerViews = this.getContainerViews(viewContainerId); containerViews.push(view.id); this.containerViews.set(viewContainerId, containerViews); @@ -424,7 +464,10 @@ export class PluginViewRegistry implements FrontendApplicationContribution { protected async createNewWebviewView(viewId: string): Promise { const webview = await this.widgetManager.getOrCreateWidget( - WebviewWidget.FACTORY_ID, { id: v4() }); + WebviewWidget.FACTORY_ID, { + id: generateUuid(), + viewId, + }); webview.setContentOptions({ allowScripts: true }); let _description: string | undefined; @@ -616,7 +659,7 @@ export class PluginViewRegistry implements FrontendApplicationContribution { if (!data) { return undefined; } - const [location] = data; + const { location } = data; const containerWidget = await this.getOrCreateViewContainerWidget(containerId); if (!containerWidget.isAttached) { await this.shell.addWidget(containerWidget, { @@ -630,7 +673,7 @@ export class PluginViewRegistry implements FrontendApplicationContribution { protected async prepareViewContainer(viewContainerId: string, containerWidget: ViewContainerWidget): Promise { const data = this.viewContainers.get(viewContainerId); if (data) { - const [, options] = data; + const { options } = data; containerWidget.setTitleOptions(options); } for (const viewId of this.getContainerViews(viewContainerId)) { @@ -893,7 +936,7 @@ export class PluginViewRegistry implements FrontendApplicationContribution { const webviewView = await this.createNewWebviewView(viewId); webviewId = webviewView.webview.identifier.id; } - const webviewWidget = this.widgetManager.getWidget(WebviewWidget.FACTORY_ID, { id: webviewId }); + const webviewWidget = this.widgetManager.getWidget(WebviewWidget.FACTORY_ID, { id: webviewId, viewId }); return webviewWidget; } diff --git a/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts b/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts index cf8d1c1a47de4..a7c755d6516d5 100644 --- a/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts +++ b/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts @@ -75,7 +75,6 @@ export class PluginViewWidget extends Panel implements StatefulWidget, Descripti this.id = this.options.id; const localContext = this.contextKeyService.createScoped(this.node); localContext.setContext('view', this.options.viewId); - this.toDispose.push(localContext); } get onDidChangeDescription(): Event { diff --git a/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx b/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx index f1f47f3aafa47..4524a90359991 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx +++ b/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx @@ -669,7 +669,7 @@ export class TreeViewWidget extends TreeViewWelcomeWidget { this.model.proxy!.$dragStarted(this.options.id, selectedNodes.map(selected => selected.id), CancellationToken.None).then(maybeUris => { if (maybeUris) { - this.applicationShell.addAdditionalDraggedEditorUris(maybeUris.map(URI.fromComponents)); + this.applicationShell.addAdditionalDraggedEditorUris(maybeUris.map(uri => URI.fromComponents(uri))); } }); } @@ -921,8 +921,7 @@ export class TreeViewWidget extends TreeViewWelcomeWidget { menuPath: contextMenuPath, anchor: { x, y }, args, - contextKeyService, - onHide: () => contextKeyService.dispose(), + contextKeyService }), 10); } } diff --git a/packages/plugin-ext/src/main/browser/view/tree-views-main.ts b/packages/plugin-ext/src/main/browser/view/tree-views-main.ts index 7809098732fbf..4d875798d5c09 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-views-main.ts +++ b/packages/plugin-ext/src/main/browser/view/tree-views-main.ts @@ -24,7 +24,6 @@ import { CompositeTreeNode, WidgetManager } from '@theia/core/lib/browser'; -import { ViewContextKeyService } from './view-context-key-service'; import { Disposable, DisposableCollection } from '@theia/core'; import { TreeViewWidget, TreeViewNode, PluginTreeModel, TreeViewWidgetOptions } from './tree-view-widget'; import { PluginViewWidget } from './plugin-view-widget'; @@ -36,7 +35,6 @@ export class TreeViewsMainImpl implements TreeViewsMain, Disposable { private readonly proxy: TreeViewsExt; private readonly viewRegistry: PluginViewRegistry; - private readonly contextKeys: ViewContextKeyService; private readonly widgetManager: WidgetManager; private readonly fileContentStore: DnDFileContentStore; @@ -50,7 +48,6 @@ export class TreeViewsMainImpl implements TreeViewsMain, Disposable { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT); this.viewRegistry = container.get(PluginViewRegistry); - this.contextKeys = this.container.get(ViewContextKeyService); this.widgetManager = this.container.get(WidgetManager); this.fileContentStore = this.container.get(DnDFileContentStore); } @@ -197,7 +194,6 @@ export class TreeViewsMainImpl implements TreeViewsMain, Disposable { })); this.toDispose.push(treeViewWidget.model.onSelectionChanged(event => { - this.contextKeys.view.set(treeViewId); this.proxy.$setSelection(treeViewId, event.map((node: TreeViewNode) => node.id)); })); diff --git a/packages/plugin-ext/src/main/browser/view/view-context-key-service.ts b/packages/plugin-ext/src/main/browser/view/view-context-key-service.ts index 943c864d46590..8bf65e0c3af6d 100644 --- a/packages/plugin-ext/src/main/browser/view/view-context-key-service.ts +++ b/packages/plugin-ext/src/main/browser/view/view-context-key-service.ts @@ -19,11 +19,6 @@ import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-k @injectable() export class ViewContextKeyService { - protected _view: ContextKey; - get view(): ContextKey { - return this._view; - } - protected _viewItem: ContextKey; get viewItem(): ContextKey { return this._viewItem; @@ -56,7 +51,6 @@ export class ViewContextKeyService { @postConstruct() protected init(): void { - this._view = this.contextKeyService.createKey('view', ''); this._viewItem = this.contextKeyService.createKey('viewItem', ''); this._activeViewlet = this.contextKeyService.createKey('activeViewlet', ''); this._activePanel = this.contextKeyService.createKey('activePanel', ''); diff --git a/packages/plugin-ext/src/main/browser/webview/pre/host.js b/packages/plugin-ext/src/main/browser/webview/pre/host.js index 7ef317406d393..c3182a1aa98d7 100644 --- a/packages/plugin-ext/src/main/browser/webview/pre/host.js +++ b/packages/plugin-ext/src/main/browser/webview/pre/host.js @@ -31,7 +31,7 @@ let sourceIsChildFrame = false; for (let i = 0; i < window.frames.length; i++) { const frame = window.frames[i]; - if (e.source === frame) { + if (e.origin === frame.origin) { sourceIsChildFrame = true; break; } diff --git a/packages/plugin-ext/src/main/browser/webview/pre/main.js b/packages/plugin-ext/src/main/browser/webview/pre/main.js index 59105add0f051..284e1c461bd44 100644 --- a/packages/plugin-ext/src/main/browser/webview/pre/main.js +++ b/packages/plugin-ext/src/main/browser/webview/pre/main.js @@ -146,7 +146,7 @@ */ function getDefaultScript(state) { return ` -const acquireVsCodeApi = (function() { +globalThis.acquireVsCodeApi = (function() { const originalPostMessage = window.parent.postMessage.bind(window.parent); const originalConsole = {...console}; const targetOrigin = '*'; @@ -198,7 +198,7 @@ const acquireVsCodeApi = (function() { }); }; })(); -const acquireTheiaApi = acquireVsCodeApi; +globalThis.acquireTheiaApi = acquireVsCodeApi; delete window.parent; delete window.top; delete window.frameElement; @@ -339,10 +339,35 @@ delete window.frameElement; clientY: e.clientY, ctrlKey: e.ctrlKey, metaKey: e.metaKey, - shiftKey: e.shiftKey + shiftKey: e.shiftKey, + // @ts-ignore the dataset should exist if the target is an element }); }; + const handleContextMenu = (e) => { + if (e.defaultPrevented) { + return; + } + + e.preventDefault(); + + host.postMessage('did-context-menu', { + clientX: e.clientX, + clientY: e.clientY, + context: findVscodeContext(e.target) + }); + }; + + function findVscodeContext(node) { + if (node) { + if (node.dataset?.vscodeContext) { + return JSON.parse(node.dataset.vscodeContext); + } + return findVscodeContext(node.parentElement); + } + return {}; + } + function preventDefaultBrowserHotkeys(e) { var isOSX = navigator.platform.toUpperCase().indexOf('MAC') >= 0; @@ -602,7 +627,7 @@ delete window.frameElement; newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown); newFrame.contentWindow.addEventListener('mousedown', handleInnerMousedown); newFrame.contentWindow.addEventListener('mouseup', handleInnerMouseup); - newFrame.contentWindow.addEventListener('contextmenu', e => e.preventDefault()); + newFrame.contentWindow.addEventListener('contextmenu', handleContextMenu); if (host.onIframeLoaded) { host.onIframeLoaded(newFrame); diff --git a/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js b/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js index c39cc3fd92828..e3a1da7cc82f7 100644 --- a/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js +++ b/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js @@ -226,7 +226,8 @@ async function processResourceRequest(event, requestUrl, resourceRoot) { parentClient.postMessage({ channel: 'load-resource', - path: resourcePath + path: resourcePath, + query: requestUrl.search.replace(/^\?/, '') }); return resourceRequestStore.create(webviewId, resourcePath) diff --git a/packages/plugin-ext/src/main/browser/webview/webview-context-keys.ts b/packages/plugin-ext/src/main/browser/webview/webview-context-keys.ts index d85da096c04b7..e5b109dc1e7f7 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview-context-keys.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview-context-keys.ts @@ -14,9 +14,10 @@ // 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 { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { ApplicationShell, FocusTracker, Widget } from '@theia/core/lib/browser'; +import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { CustomEditorWidget } from '../custom-editors/custom-editor-widget'; import { WebviewWidget } from './webview'; @injectable() @@ -27,6 +28,11 @@ export class WebviewContextKeys { */ activeWebviewPanelId: ContextKey; + /** + * Context key representing the `viewType` of the active `CustomEditorWidget`, if any. + */ + activeCustomEditorId: ContextKey; + @inject(ApplicationShell) protected applicationShell: ApplicationShell; @@ -36,12 +42,19 @@ export class WebviewContextKeys { @postConstruct() protected init(): void { this.activeWebviewPanelId = this.contextKeyService.createKey('activeWebviewPanelId', ''); + this.activeCustomEditorId = this.contextKeyService.createKey('activeCustomEditorId', ''); this.applicationShell.onDidChangeCurrentWidget(this.handleDidChangeCurrentWidget, this); } protected handleDidChangeCurrentWidget(change: FocusTracker.IChangedArgs): void { - if (change.newValue instanceof WebviewWidget) { - this.activeWebviewPanelId.set(change.newValue.viewType); + const { newValue } = change; + if (newValue instanceof CustomEditorWidget) { + this.activeCustomEditorId.set(newValue.viewType); + } else { + this.activeCustomEditorId.set(''); + } + if (newValue instanceof WebviewWidget) { + this.activeWebviewPanelId.set(newValue.viewType); } else { this.activeWebviewPanelId.set(''); } diff --git a/packages/plugin-ext/src/main/browser/webview/webview-secondary-window-support.ts b/packages/plugin-ext/src/main/browser/webview/webview-secondary-window-support.ts new file mode 100644 index 0000000000000..3a756e871fd60 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/webview/webview-secondary-window-support.ts @@ -0,0 +1,47 @@ +// ***************************************************************************** +// 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 { MaybePromise } from '@theia/core/lib/common'; +import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { SecondaryWindowHandler } from '@theia/core/lib/browser/secondary-window-handler'; +import { WebviewWidget } from './webview'; + +@injectable() +export class WebviewSecondaryWindowSupport implements FrontendApplicationContribution { + @inject(SecondaryWindowHandler) + protected readonly secondaryWindowHandler: SecondaryWindowHandler; + + onStart(app: FrontendApplication): MaybePromise { + this.secondaryWindowHandler.onDidAddWidget(([widget, win]) => { + if (widget instanceof WebviewWidget) { + const script = win.document.createElement('script'); + script.text = ` + window.addEventListener('message', e => { + // Only process messages from Theia main window + if (e.source === window.opener) { + // Delegate message to iframe + const frame = window.document.getElementsByTagName('iframe').item(0); + if (frame) { + frame.contentWindow?.postMessage({ ...e.data }, '*'); + } + } + }); `; + win.document.head.append(script); + } + }); + } +} diff --git a/packages/plugin-ext/src/main/browser/webview/webview-widget-factory.ts b/packages/plugin-ext/src/main/browser/webview/webview-widget-factory.ts index 47f4e89c0abfe..6b029253af7da 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview-widget-factory.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview-widget-factory.ts @@ -17,6 +17,7 @@ import { interfaces } from '@theia/core/shared/inversify'; import { WebviewWidget, WebviewWidgetIdentifier, WebviewWidgetExternalEndpoint } from './webview'; import { WebviewEnvironment } from './webview-environment'; +import { hashValue } from '@theia/core/lib/common/uuid'; export class WebviewWidgetFactory { @@ -30,7 +31,7 @@ export class WebviewWidgetFactory { async createWidget(identifier: WebviewWidgetIdentifier): Promise { const externalEndpoint = await this.container.get(WebviewEnvironment).externalEndpoint(); - let endpoint = externalEndpoint.replace('{{uuid}}', identifier.id); + let endpoint = externalEndpoint.replace('{{uuid}}', identifier.viewId ? hashValue(identifier.viewId) : identifier.id); if (endpoint[endpoint.length - 1] === '/') { endpoint = endpoint.slice(0, endpoint.length - 1); } diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index f7956a6ee1e71..9ed8585bfc91d 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -48,13 +48,18 @@ import { isFirefox } from '@theia/core/lib/browser/browser'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files'; import { BinaryBufferReadableStream } from '@theia/core/lib/common/buffer'; -import { ViewColumn } from '../../../plugin/types-impl'; import { ExtractableWidget } from '@theia/core/lib/browser/widgets/extractable-widget'; import { BadgeWidget } from '@theia/core/lib/browser/view-container'; +import { MenuPath } from '@theia/core'; +import { ContextMenuRenderer } from '@theia/core/lib/browser'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { PluginViewWidget } from '../view/plugin-view-widget'; // Style from core const TRANSPARENT_OVERLAY_STYLE = 'theia-transparent-overlay'; +export const WEBVIEW_CONTEXT_MENU: MenuPath = ['webview-context-menu']; + /* eslint-disable @typescript-eslint/no-explicit-any */ export const enum WebviewMessageChannels { @@ -70,7 +75,8 @@ export const enum WebviewMessageChannels { didKeydown = 'did-keydown', didMouseDown = 'did-mousedown', didMouseUp = 'did-mouseup', - onconsole = 'onconsole' + onconsole = 'onconsole', + didcontextmenu = 'did-context-menu' } export interface WebviewContentOptions { @@ -90,6 +96,7 @@ export interface WebviewConsoleLog { @injectable() export class WebviewWidgetIdentifier { id: string; + viewId?: string; } export const WebviewWidgetExternalEndpoint = Symbol('WebviewWidgetExternalEndpoint'); @@ -152,6 +159,12 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract @inject(WebviewResourceCache) protected readonly resourceCache: WebviewResourceCache; + @inject(ContextMenuRenderer) + protected readonly contextMenuRenderer: ContextMenuRenderer; + + @inject(ContextKeyService) + protected readonly contextKeyService: ContextKeyService; + viewState: WebviewPanelViewState = { visible: false, active: false, @@ -171,7 +184,6 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract } viewType: string; - viewColumn: ViewColumn; options: WebviewPanelOptions = {}; protected ready = new Deferred(); @@ -338,7 +350,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract /* no-op: webview loses focus only if another element gains focus in the main window */ })); this.toHide.push(this.on(WebviewMessageChannels.doReload, () => this.reload())); - this.toHide.push(this.on(WebviewMessageChannels.loadResource, (entry: any) => this.loadResource(entry.path))); + this.toHide.push(this.on(WebviewMessageChannels.loadResource, (entry: any) => this.loadResource(entry.path, entry.query))); this.toHide.push(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => this.loadLocalhost(entry.origin) )); @@ -357,6 +369,10 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract this.dispatchMouseEvent('mouseup', data); })); + this.toHide.push(this.on(WebviewMessageChannels.didcontextmenu, (event: { clientX: number, clientY: number, context: any }) => { + this.handleContextMenu(event); + })); + this.style(); this.toHide.push(this.themeDataProvider.onDidChangeThemeData(() => this.style())); @@ -380,6 +396,21 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract })); } + handleContextMenu(event: { clientX: number, clientY: number, context: any }): void { + const domRect = this.node.getBoundingClientRect(); + this.contextKeyService.with(this.parent instanceof PluginViewWidget ? + { webviewId: this.parent.options.viewId, ...event.context } : {}, + () => { + this.contextMenuRenderer.render({ + menuPath: WEBVIEW_CONTEXT_MENU, + args: [event.context], + anchor: { + x: domRect.x + event.clientX, y: domRect.y + event.clientY + } + }); + }); + } + protected async getRedirect(url: string): Promise { const uri = new URI(url); const localhost = this.externalUriService.parseLocalhost(uri); @@ -511,10 +542,11 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract return undefined; } - protected async loadResource(requestPath: string): Promise { - const normalizedUri = this.normalizeRequestUri(requestPath); + protected async loadResource(requestPath: string, requestQuery: string = ''): Promise { + const normalizedUri = this.normalizeRequestUri(requestPath).withQuery(decodeURIComponent(requestQuery)); // browser cache does not support file scheme, normalize to current endpoint scheme and host - const cacheUrl = new Endpoint({ path: normalizedUri.path.toString() }).getRestUrl().toString(); + // use requestPath rather than normalizedUri.path to preserve the scheme of the requested resource as a path segment + const cacheUrl = new Endpoint({ path: requestPath }).getRestUrl().withQuery(decodeURIComponent(requestQuery)).toString(); try { if (this.contentOptions.localResourceRoots) { diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts index b6b8e29760ab2..c282979ec12d6 100644 --- a/packages/plugin-ext/src/main/browser/webviews-main.ts +++ b/packages/plugin-ext/src/main/browser/webviews-main.ts @@ -23,7 +23,7 @@ import { ViewBadge, WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { WebviewWidget, WebviewWidgetIdentifier } from './webview/webview'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { ViewColumnService } from './view-column-service'; +import { ViewColumnService } from '@theia/core/lib/browser/shell/view-column-service'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; import { JSONExt } from '@theia/core/shared/@phosphor/coreutils'; import { Mutable } from '@theia/core/lib/common/types'; @@ -63,7 +63,7 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { showOptions: WebviewPanelShowOptions, options: WebviewPanelOptions & WebviewOptions ): Promise { - const view = await this.widgetManager.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: panelId }); + const view = await this.widgetManager.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: panelId, viewId: viewType }); this.hookWebview(view); view.viewType = viewType; view.title.label = title; @@ -191,7 +191,12 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { // eslint-disable-next-line @typescript-eslint/no-explicit-any async $postMessage(handle: string, value: any): Promise { - const webview = await this.getWebview(handle); + // Due to async nature of $postMessage, the webview may have been disposed in the meantime. + // Therefore, don't throw an error if the webview is not found, but return false in this case. + const webview = await this.tryGetWebview(handle); + if (!webview) { + return false; + } webview.sendMessage(value); return true; } @@ -269,7 +274,12 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { } private async tryGetWebview(id: string): Promise { - const webview = await this.widgetManager.getWidget(WebviewWidget.FACTORY_ID, { id }) + const webview = await this.widgetManager.findWidget(WebviewWidget.FACTORY_ID, options => { + if (options) { + return options.id === id; + } + return false; + }) || await this.widgetManager.getWidget(CustomEditorWidget.FACTORY_ID, { id }); return webview; } diff --git a/packages/plugin-ext/src/main/browser/window-activity-tracker.ts b/packages/plugin-ext/src/main/browser/window-activity-tracker.ts new file mode 100644 index 0000000000000..239f0f7fa1ebc --- /dev/null +++ b/packages/plugin-ext/src/main/browser/window-activity-tracker.ts @@ -0,0 +1,96 @@ +// ***************************************************************************** +// 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 { Disposable, Emitter, Event } from '@theia/core'; + +const CHECK_INACTIVITY_LIMIT = 30; +const CHECK_INACTIVITY_INTERVAL = 1000; + +const eventListenerOptions: AddEventListenerOptions = { + passive: true, + capture: true +}; +export class WindowActivityTracker implements Disposable { + + private inactivityCounter = 0; // number of times inactivity was checked since last reset + private readonly inactivityLimit = CHECK_INACTIVITY_LIMIT; // number of inactivity checks done before sending inactive signal + private readonly checkInactivityInterval = CHECK_INACTIVITY_INTERVAL; // check interval in milliseconds + private interval: NodeJS.Timeout | undefined; + + protected readonly onDidChangeActiveStateEmitter = new Emitter(); + private _activeState: boolean = true; + + constructor(readonly win: Window) { + this.initializeListeners(this.win); + } + + get onDidChangeActiveState(): Event { + return this.onDidChangeActiveStateEmitter.event; + } + + private set activeState(newState: boolean) { + if (this._activeState !== newState) { + this._activeState = newState; + this.onDidChangeActiveStateEmitter.fire(this._activeState); + } + } + + private initializeListeners(win: Window): void { + // currently assumes activity based on key/mouse/touch pressed, not on mouse move or scrolling. + win.addEventListener('mousedown', this.resetInactivity, eventListenerOptions); + win.addEventListener('keydown', this.resetInactivity, eventListenerOptions); + win.addEventListener('touchstart', this.resetInactivity, eventListenerOptions); + } + + dispose(): void { + this.stopTracking(); + this.win.removeEventListener('mousedown', this.resetInactivity); + this.win.removeEventListener('keydown', this.resetInactivity); + this.win.removeEventListener('touchstart', this.resetInactivity); + + } + + // Reset inactivity time + private resetInactivity = (): void => { + this.inactivityCounter = 0; + if (!this.interval) { + // it was not active. Set as active and restart tracking inactivity + this.activeState = true; + this.startTracking(); + } + }; + + // Check inactivity status + private checkInactivity = (): void => { + this.inactivityCounter++; + if (this.inactivityCounter >= this.inactivityLimit) { + this.activeState = false; + this.stopTracking(); + } + }; + + public startTracking(): void { + this.stopTracking(); + this.interval = setInterval(this.checkInactivity, this.checkInactivityInterval); + } + + public stopTracking(): void { + if (this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + } +} diff --git a/packages/plugin-ext/src/main/browser/window-state-main.ts b/packages/plugin-ext/src/main/browser/window-state-main.ts index c7e6800855ffc..64e9439cf2e37 100644 --- a/packages/plugin-ext/src/main/browser/window-state-main.ts +++ b/packages/plugin-ext/src/main/browser/window-state-main.ts @@ -23,6 +23,7 @@ import { UriComponents } from '../../common/uri-components'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; import { ExternalUriService } from '@theia/core/lib/browser/external-uri-service'; +import { WindowActivityTracker } from './window-activity-tracker'; export class WindowStateMain implements WindowMain, Disposable { @@ -46,6 +47,10 @@ export class WindowStateMain implements WindowMain, Disposable { const fireDidBlur = () => this.onFocusChanged(false); window.addEventListener('blur', fireDidBlur); this.toDispose.push(Disposable.create(() => window.removeEventListener('blur', fireDidBlur))); + + const tracker = new WindowActivityTracker(window); + this.toDispose.push(tracker.onDidChangeActiveState(isActive => this.onActiveStateChanged(isActive))); + this.toDispose.push(tracker); } dispose(): void { @@ -53,14 +58,18 @@ export class WindowStateMain implements WindowMain, Disposable { } private onFocusChanged(focused: boolean): void { - this.proxy.$onWindowStateChanged(focused); + this.proxy.$onDidChangeWindowFocus(focused); + } + + private onActiveStateChanged(isActive: boolean): void { + this.proxy.$onDidChangeWindowActive(isActive); } async $openUri(uriComponent: UriComponents): Promise { const uri = URI.revive(uriComponent); const url = new CoreURI(encodeURI(uri.toString(true))); try { - await open(this.openerService, url); + await open(this.openerService, url, { openExternalApp: true }); return true; } catch (e) { return false; diff --git a/packages/plugin-ext/src/main/common/basic-message-registry-main.ts b/packages/plugin-ext/src/main/common/basic-message-registry-main.ts new file mode 100644 index 0000000000000..d9e60d413c117 --- /dev/null +++ b/packages/plugin-ext/src/main/common/basic-message-registry-main.ts @@ -0,0 +1,53 @@ +// ***************************************************************************** +// Copyright (C) 2018 Red Hat, Inc. 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 { interfaces } from '@theia/core/shared/inversify'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { MessageRegistryMain, MainMessageType, MainMessageOptions, MainMessageItem } from '../../common/plugin-api-rpc'; + +/** + * A basic implementation of the message registry that does not support the modal option + * as that requires an UI. + */ +export class BasicMessageRegistryMainImpl implements MessageRegistryMain { + protected readonly messageService: MessageService; + + constructor(container: interfaces.Container) { + this.messageService = container.get(MessageService); + } + + async $showMessage(type: MainMessageType, message: string, options: MainMessageOptions, actions: MainMessageItem[]): Promise { + const action = await this.doShowMessage(type, message, options, actions); + const handle = action + ? actions.map(a => a.title).indexOf(action) + : undefined; + return handle === undefined && options.modal ? options.onCloseActionHandle : handle; + } + + protected async doShowMessage(type: MainMessageType, message: string, options: MainMessageOptions, actions: MainMessageItem[]): Promise { + // Modal notifications are not supported in this context + switch (type) { + case MainMessageType.Info: + return this.messageService.info(message, ...actions.map(a => a.title)); + case MainMessageType.Warning: + return this.messageService.warn(message, ...actions.map(a => a.title)); + case MainMessageType.Error: + return this.messageService.error(message, ...actions.map(a => a.title)); + } + throw new Error(`Message type '${type}' is not supported yet!`); + } + +} diff --git a/packages/plugin-ext/src/main/common/basic-notification-main.ts b/packages/plugin-ext/src/main/common/basic-notification-main.ts new file mode 100644 index 0000000000000..4cb73d47f0a2d --- /dev/null +++ b/packages/plugin-ext/src/main/common/basic-notification-main.ts @@ -0,0 +1,86 @@ +// ***************************************************************************** +// Copyright (C) 2018 Red Hat, Inc. 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 { NotificationExt, NotificationMain } from '../../common'; +import { ProgressService, Progress, ProgressMessage } from '@theia/core/lib/common'; +import { interfaces } from '@theia/core/shared/inversify'; +import { ProxyIdentifier, RPCProtocol } from '../../common/rpc-protocol'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; + +export class BasicNotificationMainImpl implements NotificationMain, Disposable { + protected readonly progressService: ProgressService; + protected readonly progressMap = new Map(); + protected readonly progress2Work = new Map(); + protected readonly proxy: NotificationExt; + + protected readonly toDispose = new DisposableCollection( + Disposable.create(() => { /* mark as not disposed */ }) + ); + + constructor(rpc: RPCProtocol, container: interfaces.Container, extIdentifier: ProxyIdentifier) { + this.progressService = container.get(ProgressService); + this.proxy = rpc.getProxy(extIdentifier); + } + + dispose(): void { + this.toDispose.dispose(); + } + + async $startProgress(options: NotificationMain.StartProgressOptions): Promise { + const onDidCancel = () => { + // If the map does not contain current id, it has already stopped and should not be cancelled + if (this.progressMap.has(id)) { + this.proxy.$acceptProgressCanceled(id); + } + }; + + const progressMessage = this.mapOptions(options); + const progress = await this.progressService.showProgress(progressMessage, onDidCancel); + const id = progress.id; + this.progressMap.set(id, progress); + this.progress2Work.set(id, 0); + if (this.toDispose.disposed) { + this.$stopProgress(id); + } else { + this.toDispose.push(Disposable.create(() => this.$stopProgress(id))); + } + return id; + } + protected mapOptions(options: NotificationMain.StartProgressOptions): ProgressMessage { + const { title, location, cancellable } = options; + return { text: title, options: { location, cancelable: cancellable } }; + } + + $stopProgress(id: string): void { + const progress = this.progressMap.get(id); + + if (progress) { + this.progressMap.delete(id); + this.progress2Work.delete(id); + progress.cancel(); + } + } + + $updateProgress(id: string, item: NotificationMain.ProgressReport): void { + const progress = this.progressMap.get(id); + if (!progress) { + return; + } + const done = Math.min((this.progress2Work.get(id) || 0) + (item.increment || 0), 100); + this.progress2Work.set(id, done); + progress.report({ message: item.message, work: done ? { done, total: 100 } : undefined }); + } +} diff --git a/packages/plugin-ext/src/main/common/env-main.ts b/packages/plugin-ext/src/main/common/env-main.ts new file mode 100644 index 0000000000000..a5014bd563208 --- /dev/null +++ b/packages/plugin-ext/src/main/common/env-main.ts @@ -0,0 +1,44 @@ +// ***************************************************************************** +// Copyright (C) 2018 Red Hat, Inc. 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 { interfaces } from '@theia/core/shared/inversify'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { EnvMain } from '../../common/plugin-api-rpc'; +import { isWindows, isOSX } from '@theia/core'; +import { OperatingSystem } from '../../plugin/types-impl'; + +export class EnvMainImpl implements EnvMain { + private envVariableServer: EnvVariablesServer; + + constructor(rpc: RPCProtocol, container: interfaces.Container) { + this.envVariableServer = container.get(EnvVariablesServer); + } + + $getEnvVariable(envVarName: string): Promise { + return this.envVariableServer.getValue(envVarName).then(result => result ? result.value : undefined); + } + + async $getClientOperatingSystem(): Promise { + if (isWindows) { + return OperatingSystem.Windows; + } + if (isOSX) { + return OperatingSystem.OSX; + } + return OperatingSystem.Linux; + } +} diff --git a/packages/plugin-ext/src/main/node/errors.spec.ts b/packages/plugin-ext/src/main/node/errors.spec.ts index 56a20052ba287..b013492a91296 100644 --- a/packages/plugin-ext/src/main/node/errors.spec.ts +++ b/packages/plugin-ext/src/main/node/errors.spec.ts @@ -17,13 +17,13 @@ import { rejects } from 'assert'; import { strictEqual } from 'assert/strict'; import { promises as fs } from 'fs'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import { isENOENT } from '../../common/errors'; describe('errors', () => { describe('errno-exception', () => { it('should be ENOENT error', async () => { - await rejects(fs.readFile(v4()), reason => isENOENT(reason)); + await rejects(fs.readFile(generateUuid()), reason => isENOENT(reason)); }); it('should not be ENOENT error (no code)', () => { diff --git a/packages/plugin-ext/src/main/node/handlers/plugin-theia-directory-handler.ts b/packages/plugin-ext/src/main/node/handlers/plugin-theia-directory-handler.ts index a33fa57a72f23..84c4e7025ddeb 100644 --- a/packages/plugin-ext/src/main/node/handlers/plugin-theia-directory-handler.ts +++ b/packages/plugin-ext/src/main/node/handlers/plugin-theia-directory-handler.ts @@ -28,7 +28,7 @@ import { PluginCliContribution } from '../plugin-cli-contribution'; import { getTempDirPathAsync } from '../temp-dir-util'; @injectable() -export class PluginTheiaDirectoryHandler implements PluginDeployerDirectoryHandler { +export abstract class AbstractPluginDirectoryHandler implements PluginDeployerDirectoryHandler { protected readonly deploymentDirectory: Deferred; @@ -42,15 +42,20 @@ export class PluginTheiaDirectoryHandler implements PluginDeployerDirectoryHandl async accept(resolvedPlugin: PluginDeployerEntry): Promise { - console.debug('PluginTheiaDirectoryHandler: accepting plugin with path', resolvedPlugin.path()); + console.debug(`Plugin directory handler: accepting plugin with path ${resolvedPlugin.path()}`); // handle only directories if (await resolvedPlugin.isFile()) { return false; } + // Was this directory unpacked from an NPM tarball? + const wasTarball = resolvedPlugin.originalPath().endsWith('.tgz'); + const rootPath = resolvedPlugin.path(); + const basePath = wasTarball ? path.resolve(rootPath, 'package') : rootPath; + // is there a package.json ? - const packageJsonPath = path.resolve(resolvedPlugin.path(), 'package.json'); + const packageJsonPath = path.resolve(basePath, 'package.json'); try { let packageJson = resolvedPlugin.getValue('package.json'); @@ -60,26 +65,20 @@ export class PluginTheiaDirectoryHandler implements PluginDeployerDirectoryHandl resolvedPlugin.storeValue('package.json', packageJson); } - if (packageJson?.engines?.theiaPlugin) { + if (this.acceptManifest(packageJson)) { + if (wasTarball) { + resolvedPlugin.updatePath(basePath); + resolvedPlugin.rootPath = rootPath; + } return true; } } catch { /* Failed to read file. Fall through. */ } return false; } - async handle(context: PluginDeployerDirectoryHandlerContext): Promise { - await this.copyDirectory(context); - const types: PluginDeployerEntryType[] = []; - const packageJson = context.pluginEntry().getValue('package.json'); - if (packageJson.theiaPlugin && packageJson.theiaPlugin.backend) { - types.push(PluginDeployerEntryType.BACKEND); - } - if (packageJson.theiaPlugin && packageJson.theiaPlugin.frontend) { - types.push(PluginDeployerEntryType.FRONTEND); - } + protected abstract acceptManifest(plugin: PluginPackage): boolean; - context.pluginEntry().accept(...types); - } + abstract handle(context: PluginDeployerDirectoryHandlerContext): Promise; protected async copyDirectory(context: PluginDeployerDirectoryHandlerContext): Promise { if (this.pluginCli.copyUncompressedPlugins() && context.pluginEntry().type === PluginType.User) { @@ -112,4 +111,27 @@ export class PluginTheiaDirectoryHandler implements PluginDeployerDirectoryHandl const deploymentDirectory = await this.deploymentDirectory.promise; return FileUri.fsPath(deploymentDirectory.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); } + +} + +@injectable() +export class PluginTheiaDirectoryHandler extends AbstractPluginDirectoryHandler { + + protected acceptManifest(plugin: PluginPackage): boolean { + return plugin?.engines?.theiaPlugin !== undefined; + } + + async handle(context: PluginDeployerDirectoryHandlerContext): Promise { + await this.copyDirectory(context); + const types: PluginDeployerEntryType[] = []; + const packageJson = context.pluginEntry().getValue('package.json'); + if (packageJson.theiaPlugin && packageJson.theiaPlugin.backend) { + types.push(PluginDeployerEntryType.BACKEND); + } + if (packageJson.theiaPlugin && packageJson.theiaPlugin.frontend) { + types.push(PluginDeployerEntryType.FRONTEND); + } + + context.pluginEntry().accept(...types); + } } diff --git a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts index 28983e18b15af..367bdf5aa4d01 100644 --- a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts +++ b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts @@ -21,7 +21,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util'; import { getTempDirPathAsync } from '../temp-dir-util'; import * as fs from '@theia/core/shared/fs-extra'; import * as filenamify from 'filenamify'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { PluginTheiaEnvironment } from '../../common/plugin-theia-environment'; @injectable() diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts b/packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts index 053ce001bb9df..ffc6dafc83ca0 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts @@ -29,6 +29,7 @@ export class PluginDeployerContribution implements BackendApplicationContributio protected pluginDeployer: PluginDeployer; initialize(): Promise { - return this.pluginDeployer.start(); + this.pluginDeployer.start().catch(error => this.logger.error('Initializing plugin deployer failed.', error)); + return Promise.resolve(); } } diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts index 0fe63e7b5f24f..4263a835cb4f9 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts @@ -130,8 +130,9 @@ export class PluginDeployerImpl implements PluginDeployer { id, type: PluginType.System })); + const resolvePlugins = this.measure('resolvePlugins'); const plugins = await this.resolvePlugins([...unresolvedUserEntries, ...unresolvedSystemEntries]); - deployPlugins.log('Resolve plugins list'); + resolvePlugins.log('Resolve plugins list'); await this.deployPlugins(plugins); deployPlugins.log('Deploy plugins list'); } @@ -224,8 +225,20 @@ export class PluginDeployerImpl implements PluginDeployer { } protected async resolveAndHandle(id: string, type: PluginType, options?: PluginDeployOptions): Promise { - const entries = await this.resolvePlugin(id, type, options); - await this.applyFileHandlers(entries); + let entries = await this.resolvePlugin(id, type, options); + if (type === PluginType.User) { + await this.applyFileHandlers(entries); + } else { + const filteredEntries: PluginDeployerEntry[] = []; + for (const entry of entries) { + if (await entry.isFile()) { + this.logger.warn(`Only user plugins will be handled by file handlers, please unpack the plugin '${entry.id()}' manually.`); + } else { + filteredEntries.push(entry); + } + } + entries = filteredEntries; + } await this.applyDirectoryFileHandlers(entries); return entries; } @@ -270,24 +283,27 @@ export class PluginDeployerImpl implements PluginDeployer { const acceptedPlugins = pluginsToDeploy.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted()); const acceptedFrontendPlugins = pluginsToDeploy.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted(PluginDeployerEntryType.FRONTEND)); const acceptedBackendPlugins = pluginsToDeploy.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted(PluginDeployerEntryType.BACKEND)); + const acceptedHeadlessPlugins = pluginsToDeploy.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted(PluginDeployerEntryType.HEADLESS)); this.logger.debug('the accepted plugins are', acceptedPlugins); this.logger.debug('the acceptedFrontendPlugins plugins are', acceptedFrontendPlugins); this.logger.debug('the acceptedBackendPlugins plugins are', acceptedBackendPlugins); + this.logger.debug('the acceptedHeadlessPlugins plugins are', acceptedHeadlessPlugins); acceptedPlugins.forEach(plugin => { this.logger.debug('will deploy plugin', plugin.id(), 'with changes', JSON.stringify(plugin.getChanges()), 'and this plugin has been resolved by', plugin.resolvedBy()); }); // local path to launch - const pluginPaths = acceptedBackendPlugins.map(pluginEntry => pluginEntry.path()); + const pluginPaths = [...acceptedBackendPlugins, ...acceptedHeadlessPlugins].map(pluginEntry => pluginEntry.path()); this.logger.debug('local path to deploy on remote instance', pluginPaths); - const deployments = await Promise.all([ - // start the backend plugins - this.pluginDeployerHandler.deployBackendPlugins(acceptedBackendPlugins), - this.pluginDeployerHandler.deployFrontendPlugins(acceptedFrontendPlugins) - ]); + const deployments = []; + // start the backend plugins + deployments.push(await this.pluginDeployerHandler.deployBackendPlugins(acceptedBackendPlugins)); + // headless plugins are deployed like backend plugins + deployments.push(await this.pluginDeployerHandler.deployBackendPlugins(acceptedHeadlessPlugins)); + deployments.push(await this.pluginDeployerHandler.deployFrontendPlugins(acceptedFrontendPlugins)); this.onDidDeployEmitter.fire(undefined); return deployments.reduce((accumulated, current) => accumulated += current ?? 0, 0); } diff --git a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts index fc3b6996f458d..fdb6825dc519a 100644 --- a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts +++ b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts @@ -42,6 +42,10 @@ import { PluginUninstallationManager } from './plugin-uninstallation-manager'; import { LocalizationServerImpl } from '@theia/core/lib/node/i18n/localization-server'; import { PluginLocalizationServer } from './plugin-localization-server'; import { PluginMgmtCliContribution } from './plugin-mgmt-cli-contribution'; +import { PluginRemoteCliContribution } from './plugin-remote-cli-contribution'; +import { RemoteCliContribution } from '@theia/core/lib/node/remote/remote-cli-contribution'; +import { PluginRemoteCopyContribution } from './plugin-remote-copy-contribution'; +import { RemoteCopyContribution } from '@theia/core/lib/node/remote/remote-copy-contribution'; export function bindMainBackend(bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind): void { bind(PluginApiContribution).toSelf().inSingletonScope(); @@ -89,6 +93,11 @@ export function bindMainBackend(bind: interfaces.Bind, unbind: interfaces.Unbind bind(PluginMgmtCliContribution).toSelf().inSingletonScope(); bind(CliContribution).toService(PluginMgmtCliContribution); + bind(PluginRemoteCliContribution).toSelf().inSingletonScope(); + bind(RemoteCliContribution).toService(PluginRemoteCliContribution); + bind(PluginRemoteCopyContribution).toSelf().inSingletonScope(); + bind(RemoteCopyContribution).toService(PluginRemoteCopyContribution); + bind(WebviewBackendSecurityWarnings).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(WebviewBackendSecurityWarnings); diff --git a/packages/plugin-ext/src/main/node/plugin-remote-cli-contribution.ts b/packages/plugin-ext/src/main/node/plugin-remote-cli-contribution.ts new file mode 100644 index 0000000000000..2df189ce8d57a --- /dev/null +++ b/packages/plugin-ext/src/main/node/plugin-remote-cli-contribution.ts @@ -0,0 +1,36 @@ +// ***************************************************************************** +// 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 { MaybePromise } from '@theia/core'; +import { RemoteCliContext, RemoteCliContribution } from '@theia/core/lib/node/remote/remote-cli-contribution'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { PluginCliContribution } from './plugin-cli-contribution'; + +@injectable() +export class PluginRemoteCliContribution implements RemoteCliContribution { + + @inject(PluginCliContribution) + protected readonly pluginCliContribution: PluginCliContribution; + + enhanceArgs(context: RemoteCliContext): MaybePromise { + const pluginsFolder = this.pluginCliContribution.localDir(); + if (!pluginsFolder) { + return []; + } else { + return ['--plugins=local-dir:./plugins']; + } + } +} diff --git a/packages/plugin-ext/src/main/node/plugin-remote-copy-contribution.ts b/packages/plugin-ext/src/main/node/plugin-remote-copy-contribution.ts new file mode 100644 index 0000000000000..2b7314f167118 --- /dev/null +++ b/packages/plugin-ext/src/main/node/plugin-remote-copy-contribution.ts @@ -0,0 +1,36 @@ +// ***************************************************************************** +// 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 { RemoteCopyContribution, RemoteCopyRegistry } from '@theia/core/lib/node/remote/remote-copy-contribution'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { PluginCliContribution } from './plugin-cli-contribution'; +import { FileUri } from '@theia/core/lib/common/file-uri'; + +@injectable() +export class PluginRemoteCopyContribution implements RemoteCopyContribution { + + @inject(PluginCliContribution) + protected readonly pluginCliContribution: PluginCliContribution; + + async copy(registry: RemoteCopyRegistry): Promise { + const localDir = this.pluginCliContribution.localDir(); + if (localDir) { + const fsPath = FileUri.fsPath(localDir); + await registry.directory(fsPath, 'plugins'); + } + + } +} diff --git a/packages/plugin-ext/src/main/node/plugin-service.ts b/packages/plugin-ext/src/main/node/plugin-service.ts index 91763379a5e0b..756c97975b93f 100644 --- a/packages/plugin-ext/src/main/node/plugin-service.ts +++ b/packages/plugin-ext/src/main/node/plugin-service.ts @@ -26,7 +26,7 @@ import { environment } from '@theia/core/shared/@theia/application-package/lib/e import { WsRequestValidatorContribution } from '@theia/core/lib/node/ws-request-validators'; import { MaybePromise } from '@theia/core/lib/common'; import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; -import { BackendRemoteService } from '@theia/core/lib/node/backend-remote-service'; +import { BackendRemoteService } from '@theia/core/lib/node/remote/backend-remote-service'; @injectable() export class PluginApiContribution implements BackendApplicationContribution, WsRequestValidatorContribution { diff --git a/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts b/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts index 833654c727e98..48aa75ebc3fa1 100644 --- a/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts +++ b/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts @@ -18,7 +18,7 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify' import { FileSystemLocking } from '@theia/core/lib/node'; import * as fs from '@theia/core/shared/fs-extra'; import * as path from 'path'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { PluginPaths } from './paths/const'; diff --git a/packages/plugin-ext/src/plugin/authentication-ext.ts b/packages/plugin-ext/src/plugin/authentication-ext.ts index 25dba1325d0ab..8069406f13daa 100644 --- a/packages/plugin-ext/src/plugin/authentication-ext.ts +++ b/packages/plugin-ext/src/plugin/authentication-ext.ts @@ -57,12 +57,27 @@ export class AuthenticationExtImpl implements AuthenticationExt { return this.proxy.$getSession(providerId, scopes, extensionId, extensionName, options); } + getAccounts(providerId: string): Thenable { + return this.proxy.$getAccounts(providerId); + } + registerAuthenticationProvider(id: string, label: string, provider: theia.AuthenticationProvider, options?: theia.AuthenticationProviderOptions): theia.Disposable { if (this.authenticationProviders.get(id)) { throw new Error(`An authentication provider with id '${id}' is already registered.`); } this.authenticationProviders.set(id, provider); + + provider.getSessions(undefined, {}).then(sessions => { // sessions might have been restored from secret storage + if (sessions.length > 0) { + this.proxy.$onDidChangeSessions(id, { + added: sessions, + removed: [], + changed: [] + }); + } + }); + const listener = provider.onDidChangeSessions(e => { this.proxy.$onDidChangeSessions(id, e); }); @@ -76,10 +91,10 @@ export class AuthenticationExtImpl implements AuthenticationExt { }); } - $createSession(providerId: string, scopes: string[]): Promise { + $createSession(providerId: string, scopes: string[], options: theia.AuthenticationProviderSessionOptions): Promise { const authProvider = this.authenticationProviders.get(providerId); if (authProvider) { - return Promise.resolve(authProvider.createSession(scopes)); + return Promise.resolve(authProvider.createSession(scopes, options)); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); @@ -94,10 +109,10 @@ export class AuthenticationExtImpl implements AuthenticationExt { throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } - async $getSessions(providerId: string, scopes?: string[]): Promise> { + async $getSessions(providerId: string, scopes: string[] | undefined, options: theia.AuthenticationProviderSessionOptions): Promise> { const authProvider = this.authenticationProviders.get(providerId); if (authProvider) { - const sessions = await authProvider.getSessions(scopes); + const sessions = await authProvider.getSessions(scopes, options); /* Wrap the session object received from the plugin to prevent serialization mismatches e.g. if the plugin object is constructed with the help of getters they won't be serialized: diff --git a/packages/plugin-ext/src/plugin/clipboard-ext.ts b/packages/plugin-ext/src/plugin/clipboard-ext.ts index 363f83085be88..65ed8e7d39af7 100644 --- a/packages/plugin-ext/src/plugin/clipboard-ext.ts +++ b/packages/plugin-ext/src/plugin/clipboard-ext.ts @@ -15,15 +15,21 @@ // ***************************************************************************** import * as theia from '@theia/plugin'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { RPCProtocol } from '../common/rpc-protocol'; import { PLUGIN_RPC_CONTEXT, ClipboardMain } from '../common'; +@injectable() export class ClipboardExt implements theia.Clipboard { - protected readonly proxy: ClipboardMain; + @inject(RPCProtocol) + protected readonly rpc: RPCProtocol; - constructor(rpc: RPCProtocol) { - this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.CLIPBOARD_MAIN); + protected proxy: ClipboardMain; + + @postConstruct() + initialize(): void { + this.proxy = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.CLIPBOARD_MAIN); } readText(): Promise { diff --git a/packages/plugin-ext/src/plugin/command-registry.ts b/packages/plugin-ext/src/plugin/command-registry.ts index e30576309736d..0db2f4c16c354 100644 --- a/packages/plugin-ext/src/plugin/command-registry.ts +++ b/packages/plugin-ext/src/plugin/command-registry.ts @@ -204,11 +204,16 @@ export class CommandsConverter { // eslint-disable-next-line @typescript-eslint/no-explicit-any private executeSafeCommand(...args: any[]): PromiseLike { - const command = this.commandsMap.get(args[0]); + const handle = args[0]; + if (typeof handle !== 'number') { + return Promise.reject(`Invalid handle ${handle}`); + } + const command = this.commandsMap.get(handle); if (!command || !command.command) { - return Promise.reject(`command ${args[0]} not found`); + return Promise.reject(`Safe command with handle ${handle} not found`); } - return this.commands.executeCommand(command.command, ...(command.arguments || [])); + const allArgs = (command.arguments ?? []).concat(args.slice(1)); + return this.commands.executeCommand(command.command, ...allArgs); } } diff --git a/packages/plugin-ext/src/plugin/custom-editors.ts b/packages/plugin-ext/src/plugin/custom-editors.ts index 901d05dc7f8fd..700151a37303d 100644 --- a/packages/plugin-ext/src/plugin/custom-editors.ts +++ b/packages/plugin-ext/src/plugin/custom-editors.ts @@ -25,11 +25,11 @@ import { RPCProtocol } from '../common/rpc-protocol'; import { Disposable, URI } from './types-impl'; import { UriComponents } from '../common/uri-components'; import { DocumentsExtImpl } from './documents'; -import { WebviewImpl, WebviewsExtImpl } from './webviews'; +import { WebviewsExtImpl } from './webviews'; import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; -import { WorkspaceExtImpl } from './workspace'; import { Cache } from '../common/cache'; +import * as Converters from './type-converters'; export class CustomEditorsExtImpl implements CustomEditorsExt { private readonly proxy: CustomEditorsMain; @@ -38,8 +38,7 @@ export class CustomEditorsExtImpl implements CustomEditorsExt { constructor(rpc: RPCProtocol, private readonly documentExt: DocumentsExtImpl, - private readonly webviewExt: WebviewsExtImpl, - private readonly workspace: WorkspaceExtImpl) { + private readonly webviewExt: WebviewsExtImpl) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.CUSTOM_EDITORS_MAIN); } @@ -116,22 +115,21 @@ export class CustomEditorsExtImpl implements CustomEditorsExt { document.dispose(); } - async $resolveWebviewEditor( + async $resolveWebviewEditor( resource: UriComponents, handler: string, viewType: string, title: string, - widgetOpenerOptions: T | undefined, - options: theia.WebviewPanelOptions & theia.WebviewOptions, + position: number, + options: theia.WebviewPanelOptions, cancellation: CancellationToken ): Promise { const entry = this.editorProviders.get(viewType); if (!entry) { throw new Error(`No provider found for '${viewType}'`); } - const panel = this.webviewExt.createWebviewPanel(viewType, title, {}, options, entry.plugin, handler); - const webviewOptions = WebviewImpl.toWebviewOptions(options, this.workspace, entry.plugin); - await this.proxy.$createCustomEditorPanel(handler, title, widgetOpenerOptions, webviewOptions); + const viewColumn = Converters.toViewColumn(position); + const panel = this.webviewExt.createWebviewPanel(viewType, title, { viewColumn }, options, entry.plugin, handler, false); const revivedResource = URI.revive(resource); diff --git a/packages/plugin-ext/src/plugin/debug/debug-ext.ts b/packages/plugin-ext/src/plugin/debug/debug-ext.ts index bc896a9be928a..790a16583eaa9 100644 --- a/packages/plugin-ext/src/plugin/debug/debug-ext.ts +++ b/packages/plugin-ext/src/plugin/debug/debug-ext.ts @@ -13,26 +13,28 @@ // // 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 { Emitter } from '@theia/core/lib/common/event'; import { Path } from '@theia/core/lib/common/path'; import * as theia from '@theia/plugin'; import { URI } from '@theia/core/shared/vscode-uri'; -import { Breakpoint } from '../../common/plugin-api-rpc-model'; +import { Breakpoint, DebugStackFrameDTO, DebugThreadDTO } from '../../common/plugin-api-rpc-model'; import { DebugConfigurationProviderTriggerKind, DebugExt, DebugMain, PLUGIN_RPC_CONTEXT as Ext, TerminalOptionsExt } from '../../common/plugin-api-rpc'; import { PluginPackageDebuggersContribution } from '../../common/plugin-protocol'; import { RPCProtocol } from '../../common/rpc-protocol'; import { CommandRegistryImpl } from '../command-registry'; import { ConnectionImpl } from '../../common/connection'; import { DEBUG_SCHEME, SCHEME_PATTERN } from '@theia/debug/lib/common/debug-uri-utils'; -import { Disposable, Breakpoint as BreakpointExt, SourceBreakpoint, FunctionBreakpoint, Location, Range, URI as URIImpl } from '../types-impl'; +import { Disposable, Breakpoint as BreakpointExt, SourceBreakpoint, FunctionBreakpoint, Location, Range, URI as URIImpl, DebugStackFrame, DebugThread } from '../types-impl'; import { PluginDebugAdapterSession } from './plugin-debug-adapter-session'; import { PluginDebugAdapterTracker } from './plugin-debug-adapter-tracker'; -import uuid = require('uuid'); +import { generateUuid } from '@theia/core/lib/common/uuid'; import { DebugAdapter } from '@theia/debug/lib/common/debug-model'; import { PluginDebugAdapterCreator } from './plugin-debug-adapter-creator'; import { NodeDebugAdapterCreator } from '../node/debug/plugin-node-debug-adapter-creator'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration'; +import { DebugConfiguration, DebugSessionOptions } from '@theia/debug/lib/common/debug-configuration'; +import { checkTestRunInstance } from '../tests'; interface ConfigurationProviderRecord { handle: number; @@ -43,7 +45,11 @@ interface ConfigurationProviderRecord { /* eslint-disable @typescript-eslint/no-explicit-any */ +@injectable() export class DebugExtImpl implements DebugExt { + @inject(RPCProtocol) + protected readonly rpc: RPCProtocol; + // debug sessions by sessionId private sessions = new Map(); private configurationProviderHandleGenerator: number; @@ -74,6 +80,9 @@ export class DebugExtImpl implements DebugExt { activeDebugSession: theia.DebugSession | undefined; activeDebugConsole: theia.DebugConsole; + _activeStackItem: theia.DebugStackFrame | theia.DebugThread | undefined; + private readonly onDidChangeActiveStackItemEmitter = new Emitter(); + private readonly _breakpoints = new Map(); private frontendAdapterCreator = new PluginDebugAdapterCreator(); @@ -83,8 +92,7 @@ export class DebugExtImpl implements DebugExt { return [...this._breakpoints.values()]; } - constructor(rpc: RPCProtocol) { - this.proxy = rpc.getProxy(Ext.DEBUG_MAIN); + constructor() { this.activeDebugConsole = { append: (value: string) => this.proxy.$appendToDebugConsole(value), appendLine: (value: string) => this.proxy.$appendLineToDebugConsole(value) @@ -93,6 +101,11 @@ export class DebugExtImpl implements DebugExt { this.configurationProviders = []; } + @postConstruct() + initialize(): void { + this.proxy = this.rpc.getProxy(Ext.DEBUG_MAIN); + } + /** * Sets dependencies. */ @@ -140,6 +153,10 @@ export class DebugExtImpl implements DebugExt { return this.onDidStartDebugSessionEmitter.event; } + get onDidChangeActiveStackItem(): theia.Event { + return this.onDidChangeActiveStackItemEmitter.event; + } + get onDidChangeBreakpoints(): theia.Event { return this.onDidChangeBreakpointsEmitter.event; } @@ -177,7 +194,7 @@ export class DebugExtImpl implements DebugExt { } startDebugging(folder: theia.WorkspaceFolder | undefined, nameOrConfiguration: string | theia.DebugConfiguration, options: theia.DebugSessionOptions): PromiseLike { - return this.proxy.$startDebugging(folder, nameOrConfiguration, { + const optionsDto: DebugSessionOptions = { parentSessionId: options.parentSession?.id, compact: options.compact, consoleMode: options.consoleMode, @@ -185,8 +202,16 @@ export class DebugExtImpl implements DebugExt { suppressDebugStatusbar: options.suppressDebugStatusbar, suppressDebugView: options.suppressDebugView, lifecycleManagedByParent: options.lifecycleManagedByParent, - noDebug: options.noDebug - }); + noDebug: options.noDebug, + }; + if (options.testRun) { + const run = checkTestRunInstance(options.testRun); + optionsDto.testRun = { + controllerId: run.controller.id, + runId: run.id + }; + } + return this.proxy.$startDebugging(folder, nameOrConfiguration, optionsDto); } stopDebugging(session?: theia.DebugSession): PromiseLike { @@ -253,6 +278,40 @@ export class DebugExtImpl implements DebugExt { }); } + set activeStackItem(stackItem: theia.DebugStackFrame | theia.DebugThread | undefined) { + if (this._activeStackItem === stackItem) { + return; + } + this._activeStackItem = stackItem; + this.onDidChangeActiveStackItemEmitter.fire(this.activeStackItem); + } + + get activeStackItem(): theia.DebugStackFrame | theia.DebugThread | undefined { + return this._activeStackItem; + } + + async $onDidChangeActiveThread(debugThread: DebugThreadDTO | undefined): Promise { + if (!debugThread) { + this.activeStackItem = undefined; + return; + } + const session = this.sessions.get(debugThread.sessionId); + if (session) { + this.activeStackItem = new DebugThread(session, debugThread.threadId); + } + } + + async $onDidChangeActiveFrame(debugFrame: DebugStackFrameDTO | undefined): Promise { + if (!debugFrame) { + this.activeStackItem = undefined; + return; + } + const session = this.sessions.get(debugFrame!.sessionId); + if (session) { + this.activeStackItem = new DebugStackFrame(session, debugFrame.threadId, debugFrame.frameId); + } + } + async $onSessionCustomEvent(sessionId: string, event: string, body?: any): Promise { const session = this.sessions.get(sessionId); if (session) { @@ -336,7 +395,7 @@ export class DebugExtImpl implements DebugExt { } async $createDebugSession(debugConfiguration: DebugConfiguration, workspaceFolderUri: string | undefined): Promise { - const sessionId = uuid.v4(); + const sessionId = generateUuid(); const parentSession = debugConfiguration.parentSessionId ? this.sessions.get(debugConfiguration.parentSessionId) : undefined; const theiaSession: theia.DebugSession = { diff --git a/packages/plugin-ext/src/plugin/dialogs.ts b/packages/plugin-ext/src/plugin/dialogs.ts index 129be4cc6e0af..9b01c120cef8f 100644 --- a/packages/plugin-ext/src/plugin/dialogs.ts +++ b/packages/plugin-ext/src/plugin/dialogs.ts @@ -41,7 +41,7 @@ export class DialogsExtImpl { if (result) { const uris = []; for (let i = 0; i < result.length; i++) { - const uri = URI.parse('file://' + result[i]); + const uri = URI.file(result[i]); uris.push(uri); } resolve(uris); diff --git a/packages/plugin-ext/src/plugin/documents.ts b/packages/plugin-ext/src/plugin/documents.ts index 3eee7ade3a3d6..dc6bbbefe3ce6 100644 --- a/packages/plugin-ext/src/plugin/documents.ts +++ b/packages/plugin-ext/src/plugin/documents.ts @@ -158,7 +158,7 @@ export class DocumentsExtImpl implements DocumentsExt { const uriString = uri.toString(); const data = this.editorsAndDocuments.getDocument(uriString); if (!data) { - throw new Error('unknown document'); + throw new Error('unknown document: ' + uriString); } data.acceptIsDirty(isDirty); this._onDidChangeDocument.fire({ @@ -172,7 +172,7 @@ export class DocumentsExtImpl implements DocumentsExt { const uriString = uri.toString(); const data = this.editorsAndDocuments.getDocument(uriString); if (!data) { - throw new Error('unknown document'); + throw new Error('unknown document: ' + uriString); } data.acceptIsDirty(isDirty); data.onEvents(e); diff --git a/packages/plugin-ext/src/plugin/editors-and-documents.ts b/packages/plugin-ext/src/plugin/editors-and-documents.ts index 4d3255e9f5507..23a39b050875e 100644 --- a/packages/plugin-ext/src/plugin/editors-and-documents.ts +++ b/packages/plugin-ext/src/plugin/editors-and-documents.ts @@ -14,6 +14,7 @@ // 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 { EditorsAndDocumentsExt, EditorsAndDocumentsDelta, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc'; import { TextEditorExt } from './text-editor'; import { RPCProtocol } from '../common/rpc-protocol'; @@ -24,7 +25,11 @@ import * as Converter from './type-converters'; import { dispose } from '../common/disposable-util'; import { URI } from './types-impl'; +@injectable() export class EditorsAndDocumentsExtImpl implements EditorsAndDocumentsExt { + @inject(RPCProtocol) + protected readonly rpc: RPCProtocol; + private activeEditorId: string | null = null; private readonly _onDidAddDocuments = new Emitter(); @@ -40,10 +45,11 @@ export class EditorsAndDocumentsExtImpl implements EditorsAndDocumentsExt { private readonly documents = new Map(); private readonly editors = new Map(); - constructor(private readonly rpc: RPCProtocol) { + async $acceptEditorsAndDocumentsDelta(delta: EditorsAndDocumentsDelta): Promise { + this.acceptEditorsAndDocumentsDelta(delta); } - async $acceptEditorsAndDocumentsDelta(delta: EditorsAndDocumentsDelta): Promise { + acceptEditorsAndDocumentsDelta(delta: EditorsAndDocumentsDelta): void { const removedDocuments = new Array(); const addedDocuments = new Array(); const removedEditors = new Array(); diff --git a/packages/plugin-ext/src/plugin/env.ts b/packages/plugin-ext/src/plugin/env.ts index 8fe8494a49c47..bb9c405bb6a55 100644 --- a/packages/plugin-ext/src/plugin/env.ts +++ b/packages/plugin-ext/src/plugin/env.ts @@ -14,13 +14,18 @@ // 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 * as theia from '@theia/plugin'; import { RPCProtocol } from '../common/rpc-protocol'; import { EnvMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc'; import { QueryParameters } from '../common/env'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; +@injectable() export abstract class EnvExtImpl { + @inject(RPCProtocol) + protected readonly rpc: RPCProtocol; + private proxy: EnvMain; private queryParameters: QueryParameters; private lang: string; @@ -29,15 +34,21 @@ export abstract class EnvExtImpl { private envMachineId: string; private envSessionId: string; private host: string; + private applicationRoot: string; + private appUriScheme: string; private _remoteName: string | undefined; - constructor(rpc: RPCProtocol) { - this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.ENV_MAIN); - this.envSessionId = v4(); - this.envMachineId = v4(); + constructor() { + this.envSessionId = generateUuid(); + this.envMachineId = generateUuid(); this._remoteName = undefined; } + @postConstruct() + initialize(): void { + this.proxy = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.ENV_MAIN); + } + getEnvVariable(envVarName: string): Promise { return this.proxy.$getEnvVariable(envVarName).then(x => { if (x === null) { @@ -75,6 +86,14 @@ export abstract class EnvExtImpl { this.host = appHost; } + setAppRoot(appRoot: string): void { + this.applicationRoot = appRoot; + } + + setAppUriScheme(uriScheme: string): void { + this.appUriScheme = uriScheme; + } + getClientOperatingSystem(): Promise { return this.proxy.$getClientOperatingSystem(); } @@ -83,7 +102,9 @@ export abstract class EnvExtImpl { return this.applicationName; } - abstract get appRoot(): string; + get appRoot(): string { + return this.applicationRoot; + } abstract get isNewAppInstall(): boolean; @@ -105,7 +126,7 @@ export abstract class EnvExtImpl { return this.envSessionId; } get uriScheme(): string { - return 'theia'; + return this.appUriScheme; } get uiKind(): theia.UIKind { return this.ui; diff --git a/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts b/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts index 8a9398ce6c99e..8e02f2cb97c5d 100644 --- a/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts +++ b/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts @@ -48,7 +48,7 @@ type Event = vscode.Event; type IExtensionDescription = Plugin; type IWaitUntil = WaitUntilEvent; -class FileSystemWatcher implements vscode.FileSystemWatcher { +export class FileSystemWatcher implements vscode.FileSystemWatcher { private readonly _onDidCreate = new Emitter(); private readonly _onDidChange = new Emitter(); @@ -68,7 +68,8 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { return Boolean(this._config & 0b100); } - constructor(dispatcher: Event, globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean) { + constructor(dispatcher: Event, globPattern: string | IRelativePattern, + ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean, excludes?: string[]) { this._config = 0; if (ignoreCreateEvents) { @@ -82,12 +83,13 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { } const parsedPattern = parse(globPattern); + const excludePatterns = excludes?.map(exclude => parse(exclude)) || []; const subscription = dispatcher(events => { if (!ignoreCreateEvents) { for (const created of events.created) { const uri = URI.revive(created); - if (parsedPattern(uri.fsPath)) { + if (parsedPattern(uri.fsPath) && !excludePatterns.some(p => p(uri.fsPath))) { this._onDidCreate.fire(uri); } } @@ -95,7 +97,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { if (!ignoreChangeEvents) { for (const changed of events.changed) { const uri = URI.revive(changed); - if (parsedPattern(uri.fsPath)) { + if (parsedPattern(uri.fsPath) && !excludePatterns.some(p => p(uri.fsPath))) { this._onDidChange.fire(uri); } } @@ -103,7 +105,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { if (!ignoreDeleteEvents) { for (const deleted of events.deleted) { const uri = URI.revive(deleted); - if (parsedPattern(uri.fsPath)) { + if (parsedPattern(uri.fsPath) && !excludePatterns.some(p => p(uri.fsPath))) { this._onDidDelete.fire(uri); } } @@ -113,7 +115,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { this._disposable = Disposable.from(this._onDidCreate, this._onDidChange, this._onDidDelete, subscription); } - dispose() { + dispose(): void { this._disposable.dispose(); } @@ -160,8 +162,9 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ // --- file events - createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): vscode.FileSystemWatcher { - return new FileSystemWatcher(this._onFileSystemEvent.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); + createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean, excludes?: string[]): vscode.FileSystemWatcher { + return new FileSystemWatcher(this._onFileSystemEvent.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents, excludes); } $onFileEvent(events: FileSystemEvents) { diff --git a/packages/plugin-ext/src/plugin/file-system-ext-impl.ts b/packages/plugin-ext/src/plugin/file-system-ext-impl.ts index f4db210a78ae3..b54be1a8dc093 100644 --- a/packages/plugin-ext/src/plugin/file-system-ext-impl.ts +++ b/packages/plugin-ext/src/plugin/file-system-ext-impl.ts @@ -40,7 +40,9 @@ import { State, StateMachine, LinkComputer, Edge } from '../common/link-computer import { commonPrefixLength } from '@theia/core/lib/common/strings'; import { CharCode } from '@theia/core/lib/common/char-code'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; -import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; +import { MarkdownString } from '../common/plugin-api-rpc-model'; +import { Emitter } from '@theia/core/lib/common'; +import { createAPIObject } from './plugin-context'; type IDisposable = vscode.Disposable; @@ -136,8 +138,11 @@ export class FsLinkProvider { } class ConsumerFileSystem implements vscode.FileSystem { + apiObject: vscode.FileSystem; - constructor(private _proxy: FileSystemMain, private _capabilities: Map) { } + constructor(private _proxy: FileSystemMain, private _capabilities: Map) { + this.apiObject = createAPIObject(this); + } stat(uri: vscode.Uri): Promise { return this._proxy.$stat(uri).catch(ConsumerFileSystem._handleError); @@ -209,7 +214,7 @@ export class FileSystemExtImpl implements FileSystemExt { private _handlePool: number = 0; - readonly fileSystem: vscode.FileSystem; + readonly fileSystem: ConsumerFileSystem; constructor(rpc: RPCProtocol) { this._proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_MAIN); @@ -223,7 +228,7 @@ export class FileSystemExtImpl implements FileSystemExt { this.onWillRegisterFileSystemProviderEmitter.dispose(); } - registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean } = {}) { + registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean | MarkdownString } = {}) { if (this._usedSchemes.has(scheme)) { throw new Error(`a provider for the scheme '${scheme}' is already registered`); @@ -252,7 +257,19 @@ export class FileSystemExtImpl implements FileSystemExt { capabilities += files.FileSystemProviderCapabilities.FileOpenReadWriteClose; } - this._proxy.$registerFileSystemProvider(handle, scheme, capabilities); + let readonlyMessage: MarkdownString | undefined; + if (options.isReadonly && MarkdownString.is(options.isReadonly)) { + readonlyMessage = { + value: options.isReadonly.value, + isTrusted: options.isReadonly.isTrusted, + supportThemeIcons: options.isReadonly.supportThemeIcons, + supportHtml: options.isReadonly.supportHtml, + baseUri: options.isReadonly.baseUri, + uris: options.isReadonly.uris + }; + } + + this._proxy.$registerFileSystemProvider(handle, scheme, capabilities, readonlyMessage); const subscription = provider.onDidChangeFile(event => { const mapped: IFileChangeDto[] = []; diff --git a/packages/plugin-ext/src/plugin/file-system-watcher.spec.ts b/packages/plugin-ext/src/plugin/file-system-watcher.spec.ts new file mode 100644 index 0000000000000..346abbe122b64 --- /dev/null +++ b/packages/plugin-ext/src/plugin/file-system-watcher.spec.ts @@ -0,0 +1,125 @@ +// ***************************************************************************** +// Copyright (C) 2019 Red Hat, Inc. 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 assert from 'assert'; +import { FileSystemWatcher } from './file-system-event-service-ext-impl'; +import { DisposableCollection, Emitter } from '@theia/core'; +import { FileSystemEvents } from '../common'; +import { URI } from './types-impl'; + +const eventSource = new Emitter(); +let disposables = new DisposableCollection(); + +function checkIgnore(ignoreCreate: number, ignoreChange: number, ignoreDelete: number): void { + const watcher = new FileSystemWatcher(eventSource.event, '**/*.js', !ignoreCreate, !ignoreChange, !ignoreDelete); + disposables.push(watcher); + const matching = URI.file('/foo/bar/zoz.js'); + + const changed: URI[] = []; + const created: URI[] = []; + const deleted: URI[] = []; + watcher.onDidChange(e => { + changed.push(e); + }); + + watcher.onDidCreate(e => { + created.push(e); + }); + + watcher.onDidDelete(e => { + deleted.push(e); + }); + + eventSource.fire({ changed: [matching], created: [matching], deleted: [matching] }); + + assert.equal(created.length, ignoreCreate); + assert.equal(deleted.length, ignoreDelete); + assert.equal(changed.length, ignoreChange); + +} + +describe('File Watcher Test', () => { + afterEach(() => { + disposables.dispose(); + disposables = new DisposableCollection(); + }); + + it('Should match files', () => { + const watcher = new FileSystemWatcher(eventSource.event, '**/*.js'); + disposables.push(watcher); + const matching = URI.file('/foo/bar/zoz.js'); + const notMatching = URI.file('/foo/bar/zoz.ts'); + const changed: URI[] = []; + const created: URI[] = []; + const deleted: URI[] = []; + watcher.onDidChange(e => { + changed.push(e); + }); + + watcher.onDidCreate(e => { + created.push(e); + }); + + watcher.onDidDelete(e => { + deleted.push(e); + }); + + const URIs = [matching, notMatching]; + eventSource.fire({ changed: URIs, created: URIs, deleted: URIs }); + assert.equal(matching.toString(), changed[0]?.toString()); + assert.equal(matching.toString(), created[0]?.toString()); + assert.equal(matching.toString(), deleted[0]?.toString()); + }); + + it('Should ignore created', () => { + checkIgnore(0, 1, 1); + }); + + it('Should ignore changed', () => { + checkIgnore(1, 0, 1); + }); + + it('Should ignore deleted', () => { + checkIgnore(1, 1, 0); + }); + + it('Should exclude files', () => { + const watcher = new FileSystemWatcher(eventSource.event, '**/*.js', false, false, false, ['**/bar/**']); + disposables.push(watcher); + const notMatching = URI.file('/foo/bar/zoz.js'); + const matching = URI.file('/foo/gux/zoz.js'); + const changed: URI[] = []; + const created: URI[] = []; + const deleted: URI[] = []; + watcher.onDidChange(e => { + changed.push(e); + }); + + watcher.onDidCreate(e => { + created.push(e); + }); + + watcher.onDidDelete(e => { + deleted.push(e); + }); + + const URIs = [matching, notMatching]; + eventSource.fire({ changed: URIs, created: URIs, deleted: URIs }); + assert.equal(matching.toString(), changed[0]?.toString()); + assert.equal(matching.toString(), created[0]?.toString()); + assert.equal(matching.toString(), deleted[0]?.toString()); + }); +}); diff --git a/packages/plugin-ext/src/plugin/known-commands.ts b/packages/plugin-ext/src/plugin/known-commands.ts index 26233eeaa35a1..7b63db385f364 100755 --- a/packages/plugin-ext/src/plugin/known-commands.ts +++ b/packages/plugin-ext/src/plugin/known-commands.ts @@ -261,6 +261,7 @@ export namespace KnownCommands { mappings['closeReferenceSearch'] = ['closeReferenceSearch', CONVERT_VSCODE_TO_MONACO]; mappings['goToNextReference'] = ['goToNextReference', CONVERT_VSCODE_TO_MONACO]; mappings['goToPreviousReference'] = ['goToPreviousReference', CONVERT_VSCODE_TO_MONACO]; + mappings['setContext'] = ['_setContext', CONVERT_VSCODE_TO_MONACO]; // eslint-disable-next-line @typescript-eslint/no-explicit-any const CONVERT_MONACO_TO_VSCODE = (args: any | undefined) => { @@ -296,6 +297,7 @@ export namespace KnownCommands { mappings['vscode.executeFormatDocumentProvider'] = ['vscode.executeFormatDocumentProvider', CONVERT_VSCODE_TO_MONACO, CONVERT_MONACO_TO_VSCODE]; mappings['vscode.executeFormatRangeProvider'] = ['vscode.executeFormatRangeProvider', CONVERT_VSCODE_TO_MONACO, CONVERT_MONACO_TO_VSCODE]; mappings['vscode.executeFormatOnTypeProvider'] = ['vscode.executeFormatOnTypeProvider', CONVERT_VSCODE_TO_MONACO, CONVERT_MONACO_TO_VSCODE]; + mappings['vscode.executeCodeActionProvider'] = ['vscode.executeCodeActionProvider', CONVERT_VSCODE_TO_MONACO, CONVERT_MONACO_TO_VSCODE]; mappings['vscode.prepareCallHierarchy'] = ['vscode.prepareCallHierarchy', CONVERT_VSCODE_TO_MONACO, CONVERT_MONACO_TO_VSCODE]; mappings['vscode.provideIncomingCalls'] = ['vscode.provideIncomingCalls', CONVERT_VSCODE_TO_MONACO, CONVERT_MONACO_TO_VSCODE]; mappings['vscode.provideOutgoingCalls'] = ['vscode.provideOutgoingCalls', CONVERT_VSCODE_TO_MONACO, CONVERT_MONACO_TO_VSCODE]; diff --git a/packages/plugin-ext/src/plugin/languages/diagnostics.ts b/packages/plugin-ext/src/plugin/languages/diagnostics.ts index ea74544b09ea2..5710b5bf804e6 100644 --- a/packages/plugin-ext/src/plugin/languages/diagnostics.ts +++ b/packages/plugin-ext/src/plugin/languages/diagnostics.ts @@ -22,7 +22,7 @@ import { MarkerData } from '../../common/plugin-api-rpc-model'; import { RPCProtocol } from '../../common/rpc-protocol'; import { PLUGIN_RPC_CONTEXT, LanguagesMain } from '../../common/plugin-api-rpc'; import { URI } from '@theia/core/shared/vscode-uri'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; export class DiagnosticCollection implements theia.DiagnosticCollection { private static DIAGNOSTICS_PRIORITY = [ @@ -288,7 +288,7 @@ export class Diagnostics { } private getNextId(): string { - return v4(); + return generateUuid(); } private getAllDiagnosticsForResource(uri: URI): theia.Diagnostic[] { diff --git a/packages/plugin-ext/src/plugin/languages/link-provider.ts b/packages/plugin-ext/src/plugin/languages/link-provider.ts index bb06c3be251bf..67050de012db8 100644 --- a/packages/plugin-ext/src/plugin/languages/link-provider.ts +++ b/packages/plugin-ext/src/plugin/languages/link-provider.ts @@ -33,7 +33,9 @@ export class LinkProviderAdapter { provideLinks(resource: URI, token: theia.CancellationToken): Promise { const document = this.documents.getDocumentData(resource); if (!document) { - return Promise.reject(new Error(`There is no document for ${resource}`)); + // not all documents are replicated to the plugin host (e.g. breakpoint input) + console.warn(`There is no document for ${resource}`); + return Promise.resolve(undefined); } const doc = document.document; diff --git a/packages/plugin-ext/src/plugin/localization-ext.ts b/packages/plugin-ext/src/plugin/localization-ext.ts index 0e93969a1ec95..1a70927108b70 100644 --- a/packages/plugin-ext/src/plugin/localization-ext.ts +++ b/packages/plugin-ext/src/plugin/localization-ext.ts @@ -16,6 +16,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { nls } from '@theia/core'; import { Localization } from '@theia/core/lib/common/i18n/localization'; import { LocalizationExt, LocalizationMain, Plugin, PLUGIN_RPC_CONTEXT, StringDetails } from '../common'; @@ -23,15 +24,19 @@ import { LanguagePackBundle } from '../common/language-pack-service'; import { RPCProtocol } from '../common/rpc-protocol'; import { URI } from './types-impl'; +@injectable() export class LocalizationExtImpl implements LocalizationExt { + @inject(RPCProtocol) + protected readonly rpc: RPCProtocol; - private readonly _proxy: LocalizationMain; + private _proxy: LocalizationMain; private currentLanguage?: string; private isDefaultLanguage = true; private readonly bundleCache = new Map(); - constructor(rpc: RPCProtocol) { - this._proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.LOCALIZATION_MAIN); + @postConstruct() + initialize(): void { + this._proxy = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.LOCALIZATION_MAIN); } translateMessage(pluginId: string, details: StringDetails): string { diff --git a/packages/plugin-ext/src/plugin/markdown-string.ts b/packages/plugin-ext/src/plugin/markdown-string.ts index 51dff7c905859..217cb9f1c8a98 100644 --- a/packages/plugin-ext/src/plugin/markdown-string.ts +++ b/packages/plugin-ext/src/plugin/markdown-string.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { Mutable } from '@theia/core'; -import { MarkdownStringImpl as BaseMarkdownString, MarkdownString as MarkdownStringInterface } from '@theia/core/lib/common/markdown-rendering'; +import { MarkdownStringImpl as BaseMarkdownString, MarkdownString as MarkdownStringInterface, MarkdownStringTrustedOptions } from '@theia/core/lib/common/markdown-rendering'; import * as pluginAPI from '@theia/plugin'; import { es5ClassCompat } from '../common/types'; import { URI } from './types-impl'; @@ -49,11 +49,11 @@ export class MarkdownString implements pluginAPI.MarkdownString { this.#delegate.value = value; } - get isTrusted(): boolean | undefined { + get isTrusted(): boolean | MarkdownStringTrustedOptions | undefined { return this.#delegate.isTrusted; } - set isTrusted(value: boolean | undefined) { + set isTrusted(value: boolean | MarkdownStringTrustedOptions | undefined) { this.#delegate.isTrusted = value; } diff --git a/packages/plugin-ext/src/plugin/message-registry.ts b/packages/plugin-ext/src/plugin/message-registry.ts index 016f518dec9c3..c5fb851c7f6fc 100644 --- a/packages/plugin-ext/src/plugin/message-registry.ts +++ b/packages/plugin-ext/src/plugin/message-registry.ts @@ -13,18 +13,23 @@ // // 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 { PLUGIN_RPC_CONTEXT as Ext, MessageRegistryMain, MainMessageOptions, MainMessageType } from '../common/plugin-api-rpc'; import { RPCProtocol } from '../common/rpc-protocol'; import { MessageItem, MessageOptions } from '@theia/plugin'; +@injectable() export class MessageRegistryExt { + @inject(RPCProtocol) + protected readonly rpc: RPCProtocol; private proxy: MessageRegistryMain; - constructor(rpc: RPCProtocol) { - this.proxy = rpc.getProxy(Ext.MESSAGE_REGISTRY_MAIN); + @postConstruct() + initialize(): void { + this.proxy = this.rpc.getProxy(Ext.MESSAGE_REGISTRY_MAIN); } async showMessage(type: MainMessageType, message: string, diff --git a/packages/plugin-ext/src/plugin/node/debug/debug.spec.ts b/packages/plugin-ext/src/plugin/node/debug/debug.spec.ts index 16b2c330ac252..25d28c1be2097 100644 --- a/packages/plugin-ext/src/plugin/node/debug/debug.spec.ts +++ b/packages/plugin-ext/src/plugin/node/debug/debug.spec.ts @@ -13,6 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 ********************************************************************************/ +import { Container } from '@theia/core/shared/inversify'; import { DebugSession } from '@theia/plugin'; import * as chai from 'chai'; import { ProxyIdentifier, RPCProtocol } from '../../../common/rpc-protocol'; @@ -37,7 +38,10 @@ describe('Debug API', () => { } }; - const debug = new DebugExtImpl(mockRPCProtocol); + const container = new Container(); + container.bind(RPCProtocol).toConstantValue(mockRPCProtocol); + container.bind(DebugExtImpl).toSelf().inSingletonScope(); + const debug = container.get(DebugExtImpl); it('should use sourceReference, path and sessionId', () => { const source = { diff --git a/packages/plugin-ext/src/plugin/node/env-node-ext.ts b/packages/plugin-ext/src/plugin/node/env-node-ext.ts index 65924f11b95e6..92e3f2b594d18 100644 --- a/packages/plugin-ext/src/plugin/node/env-node-ext.ts +++ b/packages/plugin-ext/src/plugin/node/env-node-ext.ts @@ -14,27 +14,29 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { injectable } from '@theia/core/shared/inversify'; import * as mac from 'macaddress'; import { EnvExtImpl } from '../env'; -import { RPCProtocol } from '../../common/rpc-protocol'; import { createHash } from 'crypto'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import fs = require('fs'); /** * Provides machineId using mac address. It's only possible on node side * Extending the common class */ +@injectable() export class EnvNodeExtImpl extends EnvExtImpl { private macMachineId: string; private _isNewAppInstall: boolean; - constructor(rpc: RPCProtocol) { - super(rpc); + constructor() { + super(); + mac.one((err, macAddress) => { if (err) { - this.macMachineId = v4(); + this.macMachineId = generateUuid(); } else { this.macMachineId = createHash('sha256').update(macAddress, 'utf8').digest('hex'); } @@ -49,13 +51,6 @@ export class EnvNodeExtImpl extends EnvExtImpl { return this.macMachineId; } - /** - * Provides application root. - */ - get appRoot(): string { - return __dirname; - } - get isNewAppInstall(): boolean { return this._isNewAppInstall; } diff --git a/packages/plugin-ext/src/plugin/node/plugin-container-module.ts b/packages/plugin-ext/src/plugin/node/plugin-container-module.ts new file mode 100644 index 0000000000000..e23285d1da569 --- /dev/null +++ b/packages/plugin-ext/src/plugin/node/plugin-container-module.ts @@ -0,0 +1,165 @@ +// ***************************************************************************** +// 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 { interfaces, ContainerModule } from '@theia/core/shared/inversify'; +import { Plugin, PluginManager, emptyPlugin } from '../../common'; + +export type ApiFactory = (plugin: Plugin) => T; + +/** + * Bind a service identifier for the factory function creating API objects of + * type `T` for a client plugin to a class providing a `call()` method that + * implements that factory function. + * + * @template T the API object type that the factory creates + * @param serviceIdentifier the injection key identifying the API factory function + * @param factoryClass the class implementing the API factory function via its `call()` method + */ +export type BindApiFactory = ( + apiModuleName: string, + serviceIdentifier: interfaces.ServiceIdentifier>, + factoryClass: new () => { createApi: ApiFactory}) => void; + +/** + * An analogue of the callback function in the constructor of the Inversify + * `ContainerModule` providing a registry that, in addition to the standard + * binding-related functions, includes a custom function for binding an + * API factory. + */ +export type PluginContainerModuleCallBack = (registry: { + bind: interfaces.Bind; + unbind: interfaces.Unbind; + isBound: interfaces.IsBound; + rebind: interfaces.Rebind; + bindApiFactory: BindApiFactory; +}) => void; + +/** + * Factory for an Inversify `ContainerModule` that supports registration of the plugin's + * API factory. Use the `PluginContainerModule`'s `create()` method to create the container + * module; its `callback` function provides a `registry` of Inversify binding functions that + * includes a `bindApiFactory` function for binding the API factory. + */ +export const PluginContainerModule: symbol & { create(callback: PluginContainerModuleCallBack): ContainerModule } = Object.assign(Symbol('PluginContainerModule'), { + create(callback: PluginContainerModuleCallBack): ContainerModule { + const result: InternalPluginContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const bindApiFactory: BindApiFactory = (apiModuleName, serviceIdentifier, factoryClass) => { + result.initializeApi = container => { + const apiCache = new PluginApiCache(apiModuleName, serviceIdentifier); + apiCache.initializeApi(container); + return apiCache; + }; + bind(factoryClass).toSelf().inSingletonScope(); + bind(serviceIdentifier).toDynamicValue(({ container }) => { + const factory = container.get(factoryClass); + return factory.createApi.bind(factory); + }).inSingletonScope(); + }; + callback({ bind, unbind, isBound, rebind, bindApiFactory }); + }); + return result; + } +}); + +/** + * Definition of additional API provided by the `ContainerModule` created by the + * {@link PluginContainerModule} factory function that is for internal use by Theia. + */ +export type InternalPluginContainerModule = ContainerModule & { + /** Use my API factory binding to initialize the plugin API in some `container`. */ + initializeApi?: (container: interfaces.Container) => PluginApiCache; +}; + +/** + * An object that creates and caches the instance of the plugin API created by the + * factory binding in a {@link PluginContainerModule} in some plugin host. + * + * @template T the custom API object's type + */ +export class PluginApiCache { + + private apiFactory: ApiFactory; + private pluginManager: PluginManager; + private defaultApi: T; + private pluginsApiImpl = new Map(); + private hookedModuleLoader = false; + + /** + * Initializes me with the module name by which plugins import the API + * and the service identifier to look up in the Inversify `Container` to + * obtain the {@link ApiFactory} that will instantiate it. + */ + constructor(private readonly apiModuleName: string, + private readonly serviceIdentifier: interfaces.ServiceIdentifier>) {} + + // Called by Theia to do any prep work needed for dishing out the API object + // when it's requested. The key part of that is hooking into the node module + // loader. This is called every time a plugin-host process is forked. + initializeApi(container: interfaces.Container): void { + this.apiFactory = container.get(this.serviceIdentifier); + this.pluginManager = container.get(PluginManager); + + if (!this.hookedModuleLoader) { + this.hookedModuleLoader = true; + this.overrideInternalLoad(); + } + } + + /** + * Hook into the override chain of JavaScript's `module` loading function + * to implement ourselves, using the API provider's registered factory, + * the construction of its default exports object. + */ + private overrideInternalLoad(): void { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const module = require('module'); + + const internalLoad = module._load; + const self = this; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + module._load = function (request: string, parent: any, isMain: any): any { + if (request !== self.apiModuleName) { + // Pass the request to the next implementation down the chain + return internalLoad.call(this, request, parent, isMain); + } + + const plugin = self.findPlugin(parent.filename); + if (plugin) { + let apiImpl = self.pluginsApiImpl.get(plugin.model.id); + if (!apiImpl) { + apiImpl = self.apiFactory(plugin); + self.pluginsApiImpl.set(plugin.model.id, apiImpl); + } + return apiImpl; + } + + console.warn( + `Extension module ${parent.filename} did an import of '${self.apiModuleName}' but our cache ` + + ' has no knowledge of that extension. Returning a generic API object; some functionality might not work correctly.' + ); + if (!self.defaultApi) { + self.defaultApi = self.apiFactory(emptyPlugin); + } + return self.defaultApi; + }; + } + + // Search all loaded plugins to see which one has the given file (absolute path) + protected findPlugin(filePath: string): Plugin | undefined { + return this.pluginManager.getAllPlugins().find(plugin => filePath.startsWith(plugin.pluginFolder)); + } +} diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-document.ts b/packages/plugin-ext/src/plugin/notebook/notebook-document.ts index a49954a6fe125..edc9a008853d0 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-document.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-document.ts @@ -26,8 +26,8 @@ import { Disposable, URI } from '@theia/core'; import * as typeConverters from '../type-converters'; import { ModelAddedData, NotebookCellDto, NotebookCellsChangedEventDto, NotebookModelAddedData, NotebookOutputDto } from '../../common'; import { NotebookRange } from '../types-impl'; -import { UriComponents } from '../../common/uri-components'; import { DocumentsExtImpl } from '../documents'; +import { UriComponents } from '../../common/uri-components'; class RawContentChangeEvent { @@ -49,7 +49,7 @@ class RawContentChangeEvent { export class Cell { - static asModelAddData(notebook: theia.NotebookDocument, cell: NotebookCellDto): ModelAddedData & { notebook: theia.NotebookDocument } { + static asModelAddData(cell: NotebookCellDto): ModelAddedData { return { EOL: cell.eol, lines: cell.source, @@ -57,7 +57,6 @@ export class Cell { uri: cell.uri, isDirty: false, versionId: 1, - notebook, modeId: '' }; } @@ -287,18 +286,19 @@ export class NotebookDocument implements Disposable { } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.Output) { this.setCellOutputs(rawEvent.index, rawEvent.outputs); relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, outputs: this.cells[rawEvent.index].apiCell.outputs }); - - // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.OutputItem) { - // this._setCellOutputItems(rawEvent.index, rawEvent.outputId, rawEvent.append, rawEvent.outputItems); - // relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, outputs: this.cells[rawEvent.index].apiCell.outputs }); + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeDocumentMetadata) { + this.metadata = result.metadata ?? {}; + // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.OutputItem) { + // this._setCellOutputItems(rawEvent.index, rawEvent.outputId, rawEvent.append, rawEvent.outputItems); + // relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, outputs: this.cells[rawEvent.index].apiCell.outputs }); } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellLanguage) { this.changeCellLanguage(rawEvent.index, rawEvent.language); relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, document: this.cells[rawEvent.index].apiCell.document }); } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellContent) { relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, document: this.cells[rawEvent.index].apiCell.document }); - // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellMime) { - // this._changeCellMime(rawEvent.index, rawEvent.mime); + // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellMime) { + // this._changeCellMime(rawEvent.index, rawEvent.mime); } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellMetadata) { this.changeCellMetadata(rawEvent.index, rawEvent.metadata); relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, metadata: this.cells[rawEvent.index].apiCell.metadata }); @@ -346,30 +346,38 @@ export class NotebookDocument implements Disposable { return; } + const addedDocuments: ModelAddedData[] = []; + const removedDocuments: UriComponents[] = []; + const contentChangeEvents: RawContentChangeEvent[] = []; - const addedCellDocuments: ModelAddedData[] = []; - const removedCellDocuments: UriComponents[] = []; splices.reverse().forEach(splice => { - const cellDtos = splice[2]; + const cellDtos = splice.newItems; const newCells = cellDtos.map((cell: NotebookCellDto) => { const extCell = new Cell(this, this.editorsAndDocuments, cell); if (!initialization) { - addedCellDocuments.push(Cell.asModelAddData(this.apiNotebook, cell)); + addedDocuments.push(Cell.asModelAddData(cell)); } return extCell; }); - const changeEvent = new RawContentChangeEvent(splice[0], splice[1], [], newCells); - const deletedItems = this.cells.splice(splice[0], splice[1], ...newCells); + const changeEvent = new RawContentChangeEvent(splice.start, splice.deleteCount, [], newCells); + const deletedItems = this.cells.splice(splice.start, splice.deleteCount, ...newCells); for (const cell of deletedItems) { - removedCellDocuments.push(cell.uri.toComponents()); changeEvent.deletedItems.push(cell.apiCell); + removedDocuments.push(cell.uri.toComponents()); } contentChangeEvents.push(changeEvent); }); + if (addedDocuments.length > 0 || removedDocuments.length > 0) { + this.editorsAndDocuments.acceptEditorsAndDocumentsDelta({ + addedDocuments, + removedDocuments + }); + } + if (bucket) { for (const changeEvent of contentChangeEvents) { bucket.push(changeEvent.asApiEvent()); diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts index f03038eabebde..e09d8fff69e22 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts @@ -18,13 +18,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '@theia/plugin'; import { - CellExecuteUpdateDto, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain, NotebookKernelSourceActionDto, NotebookOutputDto, PLUGIN_RPC_CONTEXT + CellExecuteUpdateDto, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain, + NotebookKernelSourceActionDto, NotebookOutputDto, PluginModel, PLUGIN_RPC_CONTEXT } from '../../common'; import { RPCProtocol } from '../../common/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; -import * as theia from '@theia/plugin'; import { CancellationTokenSource, Disposable, DisposableCollection, Emitter } from '@theia/core'; import { Cell } from './notebook-document'; import { NotebooksExtImpl } from './notebooks'; @@ -33,6 +32,10 @@ import { timeout, Deferred } from '@theia/core/lib/common/promise-util'; import { CellExecutionUpdateType, NotebookCellExecutionState } from '@theia/notebook/lib/common'; import { CommandRegistryImpl } from '../command-registry'; import { NotebookCellOutput, NotebookRendererScript, URI } from '../types-impl'; +import { toUriComponents } from '../../main/browser/hierarchy/hierarchy-types-converters'; +import type * as theia from '@theia/plugin'; +import { WebviewsExtImpl } from '../webviews'; +import { WorkspaceExtImpl } from '../workspace'; interface KernelData { extensionId: string; @@ -62,18 +65,31 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { constructor( rpc: RPCProtocol, private readonly notebooks: NotebooksExtImpl, - private readonly commands: CommandRegistryImpl + private readonly commands: CommandRegistryImpl, + private readonly webviews: WebviewsExtImpl, + workspace: WorkspaceExtImpl ) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_KERNELS_MAIN); + + // call onDidChangeSelection for all kernels after trust is granted to inform extensions they can set the kernel as assoiciated + // the jupyter extension for example does not set kernel association after trust is granted + workspace.onDidGrantWorkspaceTrust(() => { + this.kernelData.forEach(kernel => { + kernel.associatedNotebooks.forEach(async (_, uri) => { + const notebook = await this.notebooks.waitForNotebookDocument(URI.parse(uri)); + kernel.onDidChangeSelection.fire({ selected: true, notebook: notebook.apiNotebook }); + }); + }); + }); } private currentHandle = 0; - createNotebookController(extensionId: string, id: string, viewType: string, label: string, handler?: (cells: theia.NotebookCell[], + createNotebookController(extension: PluginModel, id: string, viewType: string, label: string, handler?: (cells: theia.NotebookCell[], notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable, rendererScripts?: NotebookRendererScript[]): theia.NotebookController { for (const kernelData of this.kernelData.values()) { - if (kernelData.controller.id === id && extensionId === kernelData.extensionId) { + if (kernelData.controller.id === id && extension.id === kernelData.extensionId) { throw new Error(`notebook controller with id '${id}' ALREADY exist`); } } @@ -81,9 +97,9 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { const handle = this.currentHandle++; const that = this; - console.debug(`NotebookController[${handle}], CREATED by ${extensionId}, ${id}`); + console.debug(`NotebookController[${handle}], CREATED by ${extension.id}, ${id}`); - const defaultExecuteHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extensionId}'`); + const defaultExecuteHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extension.id}'`); let isDisposed = false; const commandDisposables = new DisposableCollection(); @@ -92,10 +108,12 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { const onDidReceiveMessage = new Emitter<{ editor: theia.NotebookEditor; message: unknown }>(); const data: NotebookKernelDto = { - id: createKernelId(extensionId, id), + id: createKernelId(extension.id, id), notebookType: viewType, - extensionId: extensionId, - label: label || extensionId, + extensionId: extension.id, + extensionLocation: toUriComponents(extension.packageUri), + label: label || extension.id, + preloads: rendererScripts?.map(preload => ({ uri: toUriComponents(preload.uri.toString()), provides: preload.provides })) ?? [] }; // @@ -131,12 +149,11 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { get id(): string { return id; }, get notebookType(): string { return data.notebookType; }, onDidChangeSelectedNotebooks: onDidChangeSelection.event, - onDidReceiveMessage: onDidReceiveMessage.event, get label(): string { return data.label; }, set label(value) { - data.label = value ?? extensionId; + data.label = value ?? extension.id; update(); }, get detail(): string { @@ -168,11 +185,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { update(); }, get rendererScripts(): NotebookRendererScript[] { - return data.rendererScripts ?? []; - }, - set rendererScripts(value) { - data.rendererScripts = value; - update(); + return data.preloads?.map(preload => (new NotebookRendererScript(URI.from(preload.uri), preload.provides))) ?? []; }, get executeHandler(): (cells: theia.NotebookCell[], notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable { return executeHandler; @@ -197,7 +210,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { Array.from(associatedNotebooks.keys()).map(u => u.toString())); throw new Error(`notebook controller is NOT associated to notebook: ${cell.notebook.uri.toString()}`); } - return that.createNotebookCellExecution(cell, createKernelId(extensionId, this.id)); + return that.createNotebookCellExecution(cell, createKernelId(extension.id, this.id)); }, dispose: () => { if (!isDisposed) { @@ -213,16 +226,17 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { updateNotebookAffinity(notebook, priority): void { that.proxy.$updateNotebookPriority(handle, notebook.uri, priority); }, + onDidReceiveMessage: onDidReceiveMessage.event, async postMessage(message: unknown, editor?: theia.NotebookEditor): Promise { - return Promise.resolve(true); // TODO needs implementation + return that.proxy.$postMessage(handle, 'notebook:' + editor?.notebook.uri.toString(), message); }, asWebviewUri(localResource: theia.Uri): theia.Uri { - throw new Error('Method not implemented.'); + return that.webviews.toGeneralWebviewResource(extension, localResource); } }; this.kernelData.set(handle, { - extensionId: extensionId, + extensionId: extension.id, controller, onDidReceiveMessage, onDidChangeSelection, @@ -294,20 +308,20 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { }; } - $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void { + async $acceptNotebookAssociation(handle: number, uri: UriComponents, selected: boolean): Promise { const obj = this.kernelData.get(handle); if (obj) { // update data structure - const notebook = this.notebooks.getNotebookDocument(URI.from(uri))!; - if (value) { + const notebook = await this.notebooks.waitForNotebookDocument(URI.from(uri)); + if (selected) { obj.associatedNotebooks.set(notebook.uri.toString(), true); } else { obj.associatedNotebooks.delete(notebook.uri.toString()); } - console.debug(`NotebookController[${handle}] ASSOCIATE notebook`, notebook.uri.toString(), value); + console.debug(`NotebookController[${handle}] ASSOCIATE notebook`, notebook.uri.toString(), selected); // send event obj.onDidChangeSelection.fire({ - selected: value, + selected: selected, notebook: notebook.apiNotebook }); } @@ -320,7 +334,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { // extension can dispose kernels in the meantime return Promise.resolve(); } - const document = this.notebooks.getNotebookDocument(URI.from(uri)); + const document = await this.notebooks.waitForNotebookDocument(URI.from(uri)); const cells: theia.NotebookCell[] = []; for (const cellHandle of handles) { const cell = document.getCell(cellHandle); @@ -334,7 +348,6 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { await obj.controller.executeHandler.call(obj.controller, cells, document.apiNotebook, obj.controller); } catch (err) { console.error(`NotebookController[${handle}] execute cells FAILED`, err); - console.error(err); } } @@ -348,7 +361,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { // cancel or interrupt depends on the controller. When an interrupt handler is used we // don't trigger the cancelation token of executions.N - const document = this.notebooks.getNotebookDocument(URI.from(uri)); + const document = await this.notebooks.waitForNotebookDocument(URI.from(uri)); if (obj.controller.interruptHandler) { await obj.controller.interruptHandler.call(obj.controller, document.apiNotebook); @@ -377,7 +390,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { // Proposed Api though seems needed by jupyter for telemetry } - async $provideKernelSourceActions(handle: number, token: CancellationToken): Promise { + async $provideKernelSourceActions(handle: number, token: theia.CancellationToken): Promise { const provider = this.kernelSourceActionProviders.get(handle); if (provider) { const disposables = new DisposableCollection(); @@ -497,7 +510,7 @@ class NotebookCellExecutionTask implements Disposable { asApiObject(): theia.NotebookCellExecution { const that = this; const result: theia.NotebookCellExecution = { - get token(): CancellationToken { return that.tokenSource.token; }, + get token(): theia.CancellationToken { return that.tokenSource.token; }, get cell(): theia.NotebookCell { return that.cell.apiCell; }, get executionOrder(): number | undefined { return that.executionOrder; }, set executionOrder(v: number | undefined) { diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts b/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts index cbb66bb0e69f0..425a460036317 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts @@ -43,7 +43,6 @@ export class NotebookRenderersExtImpl implements NotebookRenderersExt { const messaging: theia.NotebookRendererMessaging = { onDidReceiveMessage: (listener, thisArg, disposables) => this.getOrCreateEmitterFor(rendererId).event(listener, thisArg, disposables), postMessage: (message, editorOrAlias) => { - const extHostEditor = editorOrAlias && NotebookEditor.apiEditorsToExtHost.get(editorOrAlias); return this.proxy.$postMessage(extHostEditor?.id, rendererId, message); }, diff --git a/packages/plugin-ext/src/plugin/notebook/notebooks.ts b/packages/plugin-ext/src/plugin/notebook/notebooks.ts index 773c9c3ba081b..65487fffb55aa 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebooks.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebooks.ts @@ -22,20 +22,21 @@ import { CancellationToken, Disposable, DisposableCollection, Emitter, Event, UR import { URI as TheiaURI } from '../types-impl'; import * as theia from '@theia/plugin'; import { - CommandRegistryExt, NotebookCellStatusBarListDto, NotebookDataDto, + NotebookCellStatusBarListDto, NotebookDataDto, NotebookDocumentsAndEditorsDelta, NotebookDocumentShowOptions, NotebookDocumentsMain, NotebookEditorAddData, NotebookEditorsMain, NotebooksExt, NotebooksMain, Plugin, PLUGIN_RPC_CONTEXT } from '../../common'; import { Cache } from '../../common/cache'; import { RPCProtocol } from '../../common/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; -import { CommandsConverter } from '../command-registry'; +import { CommandRegistryImpl, CommandsConverter } from '../command-registry'; import * as typeConverters from '../type-converters'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; -import { NotebookDocument } from './notebook-document'; +import { Cell, NotebookDocument } from './notebook-document'; import { NotebookEditor } from './notebook-editor'; import { EditorsAndDocumentsExtImpl } from '../editors-and-documents'; import { DocumentsExtImpl } from '../documents'; +import { CellUri, NotebookCellModelResource, NotebookModelResource } from '@theia/notebook/lib/common'; export class NotebooksExtImpl implements NotebooksExt { @@ -73,20 +74,35 @@ export class NotebooksExtImpl implements NotebooksExt { constructor( rpc: RPCProtocol, - commands: CommandRegistryExt, + commands: CommandRegistryImpl, private textDocumentsAndEditors: EditorsAndDocumentsExtImpl, - private textDocuments: DocumentsExtImpl + private textDocuments: DocumentsExtImpl, ) { + this.commandsConverter = commands.converter; this.notebookProxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN); this.notebookDocumentsProxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_MAIN); this.notebookEditors = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_EDITORS_MAIN); commands.registerArgumentProcessor({ - processArgument: (arg: { uri: URI }) => { - if (arg && arg.uri && this.documents.has(arg.uri.toString())) { - return this.documents.get(arg.uri.toString())?.apiNotebook; + processArgument: arg => { + if (NotebookModelResource.is(arg)) { + return this.documents.get(arg.notebookModelUri.toString())?.apiNotebook; + } else if (NotebookCellModelResource.is(arg)) { + const cellUri = CellUri.parse(arg.notebookCellModelUri); + if (cellUri) { + return this.documents.get(cellUri?.notebook.toString())?.getCell(cellUri.handle)?.apiCell; + } + return undefined; + } else { + return arg; } - return arg; + } + }); + + textDocumentsAndEditors.onDidChangeActiveTextEditor(e => { + if (e && e?.document.uri.scheme !== CellUri.cellUriScheme && this.activeNotebookEditor) { + this.activeNotebookEditor = undefined; + this.onDidChangeActiveNotebookEditorEmitter.fire(undefined); } }); } @@ -123,6 +139,15 @@ export class NotebooksExtImpl implements NotebooksExt { this.statusBarRegistry.delete(cacheId); } + $acceptActiveCellEditorChange(newActiveEditor: string | null): void { + const newActiveEditorId = this.textDocumentsAndEditors.allEditors().find(editor => editor.document.uri.toString() === newActiveEditor)?.id; + if (newActiveEditorId || newActiveEditor === null) { + this.textDocumentsAndEditors.acceptEditorsAndDocumentsDelta({ + newActiveEditor: newActiveEditorId ?? null + }); + } + } + // --- serialize/deserialize private currentSerializerHandle = 0; @@ -196,6 +221,7 @@ export class NotebooksExtImpl implements NotebooksExt { } async $acceptDocumentsAndEditorsDelta(delta: NotebookDocumentsAndEditorsDelta): Promise { + const removedCellDocuments: UriComponents[] = []; if (delta.removedDocuments) { for (const uri of delta.removedDocuments) { const revivedUri = URI.fromComponents(uri); @@ -205,6 +231,7 @@ export class NotebooksExtImpl implements NotebooksExt { document.dispose(); this.documents.delete(revivedUri.toString()); this.onDidCloseNotebookDocumentEmitter.fire(document.apiNotebook); + removedCellDocuments.push(...document.apiNotebook.getCells().map(cell => cell.document.uri)); } for (const editor of this.editors.values()) { @@ -215,6 +242,13 @@ export class NotebooksExtImpl implements NotebooksExt { } } + if (removedCellDocuments.length > 0) { + // publish all removed cell documents first + this.textDocumentsAndEditors.acceptEditorsAndDocumentsDelta({ + removedDocuments: removedCellDocuments + }); + } + if (delta.addedDocuments) { for (const modelData of delta.addedDocuments) { const uri = TheiaURI.from(modelData.uri); @@ -234,6 +268,14 @@ export class NotebooksExtImpl implements NotebooksExt { this.documents.get(uri.toString())?.dispose(); this.documents.set(uri.toString(), document); + if (modelData.cells.length > 0) { + // Publish new cell documents before calling the notebook document open event + // During this event, extensions might request the cell document and we want to make sure it is available + this.textDocumentsAndEditors.acceptEditorsAndDocumentsDelta({ + addedDocuments: modelData.cells.map(cell => Cell.asModelAddData(cell)) + }); + } + this.onDidOpenNotebookDocumentEmitter.fire(document.apiNotebook); } } @@ -288,14 +330,18 @@ export class NotebooksExtImpl implements NotebooksExt { if (delta.newActiveEditor === null) { // clear active notebook as current active editor is non-notebook editor this.activeNotebookEditor = undefined; + this.onDidChangeActiveNotebookEditorEmitter.fire(undefined); } else if (delta.newActiveEditor) { const activeEditor = this.editors.get(delta.newActiveEditor); if (!activeEditor) { console.error(`FAILED to find active notebook editor ${delta.newActiveEditor}`); } this.activeNotebookEditor = this.editors.get(delta.newActiveEditor); - } - if (delta.newActiveEditor !== undefined) { + if (this.textDocumentsAndEditors.activeEditor()?.document.uri.path !== this.activeNotebookEditor?.notebookData.uri.path) { + this.textDocumentsAndEditors.acceptEditorsAndDocumentsDelta({ + newActiveEditor: null + }); + } this.onDidChangeActiveNotebookEditorEmitter.fire(this.activeNotebookEditor?.apiEditor); } } @@ -310,6 +356,26 @@ export class NotebooksExtImpl implements NotebooksExt { return result; } + waitForNotebookDocument(uri: TheiaURI, duration = 2000): Promise { + const existing = this.getNotebookDocument(uri, true); + if (existing) { + return Promise.resolve(existing); + } + return new Promise((resolve, reject) => { + const listener = this.onDidOpenNotebookDocument(event => { + if (event.uri.toString() === uri.toString()) { + clearTimeout(timeout); + listener.dispose(); + resolve(this.getNotebookDocument(uri)); + } + }); + const timeout = setTimeout(() => { + listener.dispose(); + reject(new Error(`Notebook document did NOT open in ${duration}ms: ${uri}`)); + }, duration); + }); + } + private createExtHostEditor(document: NotebookDocument, editorId: string, data: NotebookEditorAddData): void { if (this.editors.has(editorId)) { @@ -327,6 +393,27 @@ export class NotebooksExtImpl implements NotebooksExt { this.editors.set(editorId, editor); } + private waitForNotebookEditor(editorId: string, duration = 2000): Promise { + const existing = this.editors.get(editorId); + if (existing) { + return Promise.resolve(existing.apiEditor); + } + return new Promise((resolve, reject) => { + const listener = this.onDidChangeVisibleNotebookEditors(() => { + const editor = this.editors.get(editorId); + if (editor) { + clearTimeout(timeout); + listener.dispose(); + resolve(editor.apiEditor); + } + }); + const timeout = setTimeout(() => { + listener.dispose(); + reject(new Error(`Notebook editor did NOT open in ${duration}ms: ${editorId}`)); + }, duration); + }); + } + async createNotebookDocument(options: { viewType: string; content?: theia.NotebookData }): Promise { const canonicalUri = await this.notebookDocumentsProxy.$tryCreateNotebook({ viewType: options.viewType, @@ -350,7 +437,7 @@ export class NotebooksExtImpl implements NotebooksExt { notebookOrUri = await this.openNotebookDocument(notebookOrUri as TheiaURI); } - const notebook = notebookOrUri as theia.NotebookDocument; + const notebook = notebookOrUri; let resolvedOptions: NotebookDocumentShowOptions; if (typeof options === 'object') { @@ -367,7 +454,7 @@ export class NotebooksExtImpl implements NotebooksExt { } const editorId = await this.notebookEditors.$tryShowNotebookDocument(notebook.uri, notebook.notebookType, resolvedOptions); - const editor = editorId && this.editors.get(editorId)?.apiEditor; + const editor = editorId && await this.waitForNotebookEditor(editorId); if (editor) { return editor; diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 76a6a87da7c89..e027b45d6b84c 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -19,7 +19,7 @@ import type * as theia from '@theia/plugin'; import { CommandRegistryImpl } from './command-registry'; -import { Emitter } from '@theia/core/lib/common/event'; +import { Emitter, Event } from '@theia/core/lib/common/event'; import { CancellationError, CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { QuickOpenExtImpl } from './quick-open'; import { @@ -87,6 +87,7 @@ import { InlineValueContext, DocumentHighlightKind, DocumentHighlight, + MultiDocumentHighlight, DocumentLink, DocumentDropEdit, CodeLens, @@ -123,6 +124,8 @@ import { Breakpoint, SourceBreakpoint, FunctionBreakpoint, + DebugStackFrame, + DebugThread, FoldingRange, FoldingRangeKind, SelectionRange, @@ -184,6 +187,7 @@ import { TestTag, TestRunRequest, TestMessage, + TestMessageStackFrame, ExtensionKind, InlineCompletionItem, InlineCompletionList, @@ -197,12 +201,36 @@ import { TextMergeTabInput, WebviewEditorTabInput, DocumentPasteEdit, + DocumentPasteEditKind, + DocumentPasteTriggerKind, + DocumentDropOrPasteEditKind, ExternalUriOpenerPriority, EditSessionIdentityMatch, TerminalOutputAnchor, - TerminalQuickFixExecuteTerminalCommand, + TerminalQuickFixTerminalCommand, TerminalQuickFixOpener, - TestResultState + TestResultState, + BranchCoverage, + DeclarationCoverage, + FileCoverage, + StatementCoverage, + TestCoverageCount, + ChatRequestTurn, + ChatResponseTurn, + ChatResponseAnchorPart, + ChatResponseCommandButtonPart, + ChatResponseFileTreePart, + ChatResponseMarkdownPart, + ChatResponseProgressPart, + ChatResponseReferencePart, + ChatResultFeedbackKind, + LanguageModelChatMessage, + LanguageModelChatMessageRole, + LanguageModelError, + PortAutoForwardAction, + PortAttributes, + DebugVisualization, + TerminalShellExecutionCommandLineConfidence } from './types-impl'; import { AuthenticationExtImpl } from './authentication-ext'; import { SymbolKind } from '../common/plugin-api-rpc-model'; @@ -249,6 +277,23 @@ import { NotebookKernelsExtImpl } from './notebook/notebook-kernels'; import { NotebookDocumentsExtImpl } from './notebook/notebook-documents'; import { NotebookEditorsExtImpl } from './notebook/notebook-editors'; import { TestingExtImpl } from './tests'; +import { UriExtImpl } from './uri-ext'; +import { isObject } from '@theia/core'; + +export function createAPIObject(rawObject: T): T { + return new Proxy(rawObject, { + get(target, p, receiver) { + const isOwnProperty = !!Object.getOwnPropertyDescriptor(target, p); + const val = Reflect.get(target, p); + if (!isOwnProperty && typeof val === 'function') { + // bind functions that are inherited from the prototype to the object itself. + // This should handle the case of events. + return val.bind(target); + } + return val; + }, + }) as T; +} export function createAPIFactory( rpc: RPCProtocol, @@ -275,7 +320,7 @@ export function createAPIFactory( const notebooksExt = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT, new NotebooksExtImpl(rpc, commandRegistry, editorsAndDocumentsExt, documents)); const notebookEditors = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_EDITORS_EXT, new NotebookEditorsExtImpl(notebooksExt)); const notebookRenderers = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_EXT, new NotebookRenderersExtImpl(rpc, notebooksExt)); - const notebookKernels = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT, new NotebookKernelsExtImpl(rpc, notebooksExt, commandRegistry)); + const notebookKernels = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT, new NotebookKernelsExtImpl(rpc, notebooksExt, commandRegistry, webviewExt, workspaceExt)); const notebookDocuments = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_EXT, new NotebookDocumentsExtImpl(notebooksExt)); const statusBarMessageRegistryExt = new StatusBarMessageRegistryExt(rpc); const terminalExt = rpc.set(MAIN_RPC_CONTEXT.TERMINAL_EXT, new TerminalServiceExtImpl(rpc)); @@ -293,10 +338,11 @@ export function createAPIFactory( const themingExt = rpc.set(MAIN_RPC_CONTEXT.THEMING_EXT, new ThemingExtImpl(rpc)); const commentsExt = rpc.set(MAIN_RPC_CONTEXT.COMMENTS_EXT, new CommentsExtImpl(rpc, commandRegistry, documents)); const tabsExt = rpc.set(MAIN_RPC_CONTEXT.TABS_EXT, new TabsExtImpl(rpc)); - const customEditorExt = rpc.set(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT, new CustomEditorsExtImpl(rpc, documents, webviewExt, workspaceExt)); + const customEditorExt = rpc.set(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT, new CustomEditorsExtImpl(rpc, documents, webviewExt)); const webviewViewsExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEW_VIEWS_EXT, new WebviewViewsExtImpl(rpc, webviewExt)); const telemetryExt = rpc.set(MAIN_RPC_CONTEXT.TELEMETRY_EXT, new TelemetryExtImpl()); const testingExt = rpc.set(MAIN_RPC_CONTEXT.TESTING_EXT, new TestingExtImpl(rpc, commandRegistry)); + const uriExt = rpc.set(MAIN_RPC_CONTEXT.URI_EXT, new UriExtImpl(rpc)); rpc.set(MAIN_RPC_CONTEXT.DEBUG_EXT, debugExt); return function (plugin: InternalPlugin): typeof theia { @@ -309,6 +355,9 @@ export function createAPIFactory( }, get onDidChangeSessions(): theia.Event { return authenticationExt.onDidChangeSessions; + }, + getAccounts(providerId: string): Thenable { + return authenticationExt.getAccounts(providerId); } }; function commandIsDeclaredInPackage(id: string, model: PluginPackage): boolean { @@ -460,10 +509,11 @@ export function createAPIFactory( }, // eslint-disable-next-line @typescript-eslint/no-explicit-any showQuickPick(items: any, options?: theia.QuickPickOptions, token?: theia.CancellationToken): any { - return quickOpenExt.showQuickPick(items, options, token); + return quickOpenExt.showQuickPick(plugin, items, options, token); }, createQuickPick(): theia.QuickPick { - return quickOpenExt.createQuickPick(plugin); + + return createAPIObject(quickOpenExt.createQuickPick(plugin)); }, showWorkspaceFolderPick(options?: theia.WorkspaceFolderPickOptions): PromiseLike { return workspaceExt.pickWorkspaceFolder(options); @@ -502,9 +552,12 @@ export function createAPIFactory( priority = priorityOrAlignment; } + // TODO: here return statusBarMessageRegistryExt.createStatusBarItem(alignment, priority, id); }, createOutputChannel(name: string, options?: { log: true }): any { + + // TODO: here return !options ? outputChannelRegistryExt.createOutputChannel(name, pluginToPluginInfo(plugin)) : outputChannelRegistryExt.createOutputChannel(name, pluginToPluginInfo(plugin), options); @@ -513,7 +566,7 @@ export function createAPIFactory( title: string, showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions, options: theia.WebviewPanelOptions & theia.WebviewOptions = {}): theia.WebviewPanel { - return webviewExt.createWebview(viewType, title, showOptions, options, plugin); + return createAPIObject(webviewExt.createWebview(viewType, title, showOptions, options, plugin)); }, registerWebviewPanelSerializer(viewType: string, serializer: theia.WebviewPanelSerializer): theia.Disposable { return webviewExt.registerWebviewPanelSerializer(viewType, serializer, plugin); @@ -541,19 +594,19 @@ export function createAPIFactory( createTerminal(nameOrOptions: theia.TerminalOptions | theia.ExtensionTerminalOptions | theia.ExtensionTerminalOptions | (string | undefined), shellPath?: string, shellArgs?: string[] | string): theia.Terminal { - return terminalExt.createTerminal(nameOrOptions, shellPath, shellArgs); + return createAPIObject(terminalExt.createTerminal(plugin, nameOrOptions, shellPath, shellArgs)); }, onDidChangeTerminalState, onDidCloseTerminal, onDidOpenTerminal, createTextEditorDecorationType(options: theia.DecorationRenderOptions): theia.TextEditorDecorationType { - return editors.createTextEditorDecorationType(options); + return createAPIObject(editors.createTextEditorDecorationType(options)); }, registerTreeDataProvider(viewId: string, treeDataProvider: theia.TreeDataProvider): Disposable { return treeViewsExt.registerTreeDataProvider(plugin, viewId, treeDataProvider); }, createTreeView(viewId: string, options: theia.TreeViewOptions): theia.TreeView { - return treeViewsExt.createTreeView(plugin, viewId, options); + return createAPIObject(treeViewsExt.createTreeView(plugin, viewId, options)); }, withScmProgress(task: (progress: theia.Progress) => Thenable) { const options: ProgressOptions = { location: ProgressLocation.SourceControl }; @@ -569,11 +622,10 @@ export function createAPIFactory( return decorationsExt.registerFileDecorationProvider(provider, pluginToPluginInfo(plugin)); }, registerUriHandler(handler: theia.UriHandler): theia.Disposable { - // TODO ? - return new Disposable(() => { }); + return uriExt.registerUriHandler(handler, pluginToPluginInfo(plugin)); }, createInputBox(): theia.InputBox { - return quickOpenExt.createInputBox(plugin); + return createAPIObject(quickOpenExt.createInputBox(plugin)); }, registerTerminalLinkProvider(provider: theia.TerminalLinkProvider): theia.Disposable { return terminalExt.registerTerminalLinkProvider(provider); @@ -602,14 +654,42 @@ export function createAPIFactory( registerTerminalQuickFixProvider(id: string, provider: theia.TerminalQuickFixProvider): theia.Disposable { return terminalExt.registerTerminalQuickFixProvider(id, provider); }, + + /** Theia-specific TerminalObserver */ + registerTerminalObserver(observer: theia.TerminalObserver): theia.Disposable { + return terminalExt.registerTerminalObserver(observer); + }, + /** @stubbed ShareProvider */ registerShareProvider: () => Disposable.NULL, + /** @stubbed Terminal Shell Ingration */ + onDidChangeTerminalShellIntegration: Event.None, + /** @stubbed Terminal Shell Ingration */ + onDidEndTerminalShellExecution: Event.None, + /** @stubbed Terminal Shell Ingration */ + onDidStartTerminalShellExecution: Event.None }; + function createFileSystemWatcher(pattern: RelativePattern, options?: theia.FileSystemWatcherOptions): theia.FileSystemWatcher; + function createFileSystemWatcher(pattern: theia.GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: + boolean, ignoreDeleteEvents?: boolean): theia.FileSystemWatcher; + function createFileSystemWatcher(pattern: RelativePattern | theia.GlobPattern, + ignoreCreateOrOptions?: theia.FileSystemWatcherOptions | boolean, ignoreChangeEventsBoolean?: boolean, ignoreDeleteEventsBoolean?: boolean): theia.FileSystemWatcher { + if (isObject(ignoreCreateOrOptions)) { + const { ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents, excludes } = (ignoreCreateOrOptions as theia.FileSystemWatcherOptions); + return createAPIObject( + extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), + ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents, excludes)); + } else { + return createAPIObject( + extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), + ignoreCreateOrOptions as boolean, ignoreChangeEventsBoolean, ignoreDeleteEventsBoolean)); + } + } const workspace: typeof theia.workspace = { get fs(): theia.FileSystem { - return fileSystemExt.fileSystem; + return fileSystemExt.fileSystem.apiObject; }, get rootPath(): string | undefined { @@ -708,11 +788,10 @@ export function createAPIFactory( } else { throw new Error('Invalid arguments'); } + // Notebook extension will create a document in openNotebookDocument() or create openNotebookDocument() return notebooksExt.getNotebookDocument(uri).apiNotebook; - }, - createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): theia.FileSystemWatcher => - extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), ignoreCreate, ignoreChange, ignoreDelete), + createFileSystemWatcher, findFiles(include: theia.GlobPattern, exclude?: theia.GlobPattern | null, maxResults?: number, token?: CancellationToken): PromiseLike { return workspaceExt.findFiles(include, exclude, maxResults, token); }, @@ -720,6 +799,13 @@ export function createAPIFactory( callbackOrToken?: CancellationToken | ((result: theia.TextSearchResult) => void), token?: CancellationToken): Promise { return workspaceExt.findTextInFiles(query, optionsOrCallback, callbackOrToken, token); }, + save(uri: theia.Uri): PromiseLike { + return editors.save(uri); + }, + + saveAs(uri: theia.Uri): PromiseLike { + return editors.saveAs(uri); + }, saveAll(includeUntitled?: boolean): PromiseLike { return editors.saveAll(includeUntitled); }, @@ -729,7 +815,8 @@ export function createAPIFactory( registerTextDocumentContentProvider(scheme: string, provider: theia.TextDocumentContentProvider): theia.Disposable { return workspaceExt.registerTextDocumentContentProvider(scheme, provider); }, - registerFileSystemProvider(scheme: string, provider: theia.FileSystemProvider, options?: { isCaseSensitive?: boolean, isReadonly?: boolean }): theia.Disposable { + registerFileSystemProvider(scheme: string, provider: theia.FileSystemProvider, options?: { isCaseSensitive?: boolean, isReadonly?: boolean | MarkdownString }): + theia.Disposable { return fileSystemExt.registerFileSystemProvider(scheme, provider, options); }, getWorkspaceFolder(uri: theia.Uri): theia.WorkspaceFolder | undefined { @@ -776,7 +863,13 @@ export function createAPIFactory( }, getCanonicalUri(uri: theia.Uri, options: theia.CanonicalUriRequestOptions, token: CancellationToken): theia.ProviderResult { return workspaceExt.getCanonicalUri(uri, options, token); - } + }, + /** + * @stubbed + * This is a stub implementation, that should minimally satisfy vscode extensions + * that currently use this proposed API. + */ + registerPortAttributesProvider: () => Disposable.NULL }; const onDidChangeLogLevel = new Emitter(); @@ -791,7 +884,7 @@ export function createAPIFactory( return telemetryExt.onDidChangeTelemetryEnabled; }, createTelemetryLogger(sender: theia.TelemetrySender, options?: theia.TelemetryLoggerOptions): theia.TelemetryLogger { - return telemetryExt.createTelemetryLogger(sender, options); + return createAPIObject(telemetryExt.createTelemetryLogger(sender, options)); }, get remoteName(): string | undefined { return envExt.remoteName; }, get machineId(): string { return envExt.machineId; }, @@ -827,7 +920,7 @@ export function createAPIFactory( const extensions: typeof theia.extensions = Object.freeze({ // eslint-disable-next-line @typescript-eslint/no-explicit-any - getExtension(extensionId: string, includeFromDifferentExtensionHosts: boolean = false): theia.Extension | undefined { + getExtension(extensionId: string, includeFromDifferentExtensionHosts: boolean = false): theia.Extension | undefined { includeFromDifferentExtensionHosts = false; const plg = pluginManager.getPluginById(extensionId.toLowerCase()); if (plg) { @@ -866,7 +959,7 @@ export function createAPIFactory( return languagesExt.getDiagnostics(resource); }, createDiagnosticCollection(name?: string): theia.DiagnosticCollection { - return languagesExt.createDiagnosticCollection(name); + return createAPIObject(languagesExt.createDiagnosticCollection(name)); }, setLanguageConfiguration(language: string, configuration: theia.LanguageConfiguration): theia.Disposable { return languagesExt.setLanguageConfiguration(language, configuration); @@ -916,6 +1009,13 @@ export function createAPIFactory( registerDocumentHighlightProvider(selector: theia.DocumentSelector, provider: theia.DocumentHighlightProvider): theia.Disposable { return languagesExt.registerDocumentHighlightProvider(selector, provider, pluginToPluginInfo(plugin)); }, + /** + * @stubbed + * @monaco-uplift: wait until API is available in Monaco (1.85.0+) + */ + registerMultiDocumentHighlightProvider(selector: theia.DocumentSelector, provider: theia.MultiDocumentHighlightProvider): theia.Disposable { + return Disposable.NULL; + }, registerWorkspaceSymbolProvider(provider: theia.WorkspaceSymbolProvider): theia.Disposable { return languagesExt.registerWorkspaceSymbolProvider(provider, pluginToPluginInfo(plugin)); }, @@ -996,7 +1096,7 @@ export function createAPIFactory( const tests: typeof theia.tests = { createTestController(id, label: string) { - return testingExt.createTestController(id, label); + return createAPIObject(testingExt.createTestController(id, label)); } }; /* End of Tests API */ @@ -1047,6 +1147,12 @@ export function createAPIFactory( get onDidChangeBreakpoints(): theia.Event { return debugExt.onDidChangeBreakpoints; }, + get activeStackItem(): DebugThread | DebugStackFrame | undefined { + return debugExt.activeStackItem; + }, + get onDidChangeActiveStackItem(): theia.Event { + return debugExt.onDidChangeActiveStackItem; + }, registerDebugAdapterDescriptorFactory(debugType: string, factory: theia.DebugAdapterDescriptorFactory): Disposable { return debugExt.registerDebugAdapterDescriptorFactory(debugType, factory); }, @@ -1081,7 +1187,11 @@ export function createAPIFactory( }, asDebugSourceUri(source: theia.DebugProtocolSource, session?: theia.DebugSession): theia.Uri { return debugExt.asDebugSourceUri(source, session); - } + }, + /** @stubbed Due to proposed API */ + registerDebugVisualizationProvider: () => Disposable.NULL, + /** @stubbed Due to proposed API */ + registerDebugVisualizationTreeProvider: () => Disposable.NULL }; const tasks: typeof theia.tasks = { @@ -1098,6 +1208,7 @@ export function createAPIFactory( }, get taskExecutions(): ReadonlyArray { + // TODO: here return tasksExt.taskExecutions; }, onDidStartTask(listener, thisArg?, disposables?) { @@ -1118,19 +1229,19 @@ export function createAPIFactory( get inputBox(): theia.SourceControlInputBox { const inputBox = scmExt.getLastInputBox(plugin); if (inputBox) { - return inputBox; + return inputBox.apiObject; } else { throw new Error('Input box not found!'); } }, createSourceControl(id: string, label: string, rootUri?: URI): theia.SourceControl { - return scmExt.createSourceControl(plugin, id, label, rootUri); + return createAPIObject(scmExt.createSourceControl(plugin, id, label, rootUri)); } }; const comments: typeof theia.comments = { createCommentController(id: string, label: string): theia.CommentController { - return commentsExt.createCommentController(plugin, id, label); + return createAPIObject(commentsExt.createCommentController(plugin, id, label)); } }; @@ -1167,7 +1278,7 @@ export function createAPIFactory( controller: theia.NotebookController) => void | Thenable, rendererScripts?: NotebookRendererScript[] ) { - return notebookKernels.createNotebookController(plugin.model.id, id, notebookType, label, handler, rendererScripts); + return notebookKernels.createNotebookController(plugin.model, id, notebookType, label, handler, rendererScripts); }, createRendererMessaging(rendererId) { return notebookRenderers.createRendererMessaging(rendererId); @@ -1188,9 +1299,35 @@ export function createAPIFactory( } }; + const chat: typeof theia.chat = { + /** @stubbed MappedEditsProvider */ + registerMappedEditsProvider(documentSelector: theia.DocumentSelector, provider: theia.MappedEditsProvider): Disposable { + return Disposable.NULL; + }, + /** @stubbed ChatRequestHandler */ + createChatParticipant(id: string, handler: theia.ChatRequestHandler): theia.ChatParticipant { + return { + id, + requestHandler: handler, + dispose() { }, + onDidReceiveFeedback: (listener, thisArgs?, disposables?) => Event.None(listener, thisArgs, disposables) + }; + } + }; + + const lm: typeof theia.lm = { + /** @stubbed LanguageModelChat */ + selectChatModels(selector?: theia.LanguageModelChatSelector): Thenable { + return Promise.resolve([]); + }, + /** @stubbed LanguageModelChat */ + onDidChangeChatModels: (listener, thisArgs?, disposables?) => Event.None(listener, thisArgs, disposables) + }; + return { version: require('../../package.json').version, authentication, + chat, commands, comments, window, @@ -1205,6 +1342,7 @@ export function createAPIFactory( notebooks, l10n, tests, + lm, // Types StatusBarAlignment: StatusBarAlignment, Disposable: Disposable, @@ -1261,8 +1399,10 @@ export function createAPIFactory( InlineValueContext, DocumentHighlightKind, DocumentHighlight, + MultiDocumentHighlight, DocumentLink, DocumentDropEdit, + DocumentDropOrPasteEditKind, CodeLens, CodeActionKind, CodeActionTrigger, @@ -1300,6 +1440,8 @@ export function createAPIFactory( Breakpoint, SourceBreakpoint, FunctionBreakpoint, + DebugStackFrame, + DebugThread, Color, ColorInformation, ColorPresentation, @@ -1358,6 +1500,7 @@ export function createAPIFactory( TestTag, TestRunRequest, TestMessage, + TestMessageStackFrame, ExtensionKind, InlineCompletionItem, InlineCompletionList, @@ -1374,11 +1517,34 @@ export function createAPIFactory( TerminalOutputAnchor, TerminalExitReason, DocumentPasteEdit, + DocumentPasteEditKind, + DocumentPasteTriggerKind, ExternalUriOpenerPriority, - TerminalQuickFixExecuteTerminalCommand, + TerminalQuickFixTerminalCommand, TerminalQuickFixOpener, EditSessionIdentityMatch, - TestResultState + TestResultState, + BranchCoverage, + DeclarationCoverage, + FileCoverage, + StatementCoverage, + TestCoverageCount, + ChatRequestTurn, + ChatResponseTurn, + ChatResponseAnchorPart, + ChatResponseCommandButtonPart, + ChatResponseFileTreePart, + ChatResponseMarkdownPart, + ChatResponseProgressPart, + ChatResponseReferencePart, + ChatResultFeedbackKind, + LanguageModelChatMessage, + LanguageModelChatMessageRole, + LanguageModelError, + PortAutoForwardAction, + PortAttributes, + DebugVisualization, + TerminalShellExecutionCommandLineConfidence }; }; } @@ -1460,7 +1626,7 @@ export class PluginExt extends Plugin implements ExtensionPlugin { this.extensionPath = this.pluginPath; this.extensionUri = this.pluginUri; - this.extensionKind = ExtensionKind.UI; // stub as a local extension (not running on a remote workspace) + this.extensionKind = pluginManager.getPluginKind(); this.isFromDifferentExtensionHost = isFromDifferentExtensionHost; } diff --git a/packages/plugin-ext/src/plugin/plugin-icon-path.ts b/packages/plugin-ext/src/plugin/plugin-icon-path.ts index 610e7dcc1b3af..54b7b034a2aac 100644 --- a/packages/plugin-ext/src/plugin/plugin-icon-path.ts +++ b/packages/plugin-ext/src/plugin/plugin-icon-path.ts @@ -24,8 +24,8 @@ export type PluginIconPath = string | URI | { dark: string | URI }; export namespace PluginIconPath { - export function toUrl(iconPath: PluginIconPath | undefined, plugin: Plugin): IconUrl | undefined { - if (!iconPath) { + export function toUrl(iconPath: unknown, plugin: Plugin): IconUrl | undefined { + if (!is(iconPath)) { return undefined; } if (typeof iconPath === 'object' && 'light' in iconPath) { @@ -36,6 +36,9 @@ export namespace PluginIconPath { } return asString(iconPath, plugin); } + export function is(item: unknown): item is PluginIconPath { + return typeof item === 'string' || item instanceof URI || typeof item === 'object' && !!item && 'light' in item && 'dark' in item; + } export function asString(arg: string | URI, plugin: Plugin): string { arg = arg instanceof URI && arg.scheme === 'file' ? arg.fsPath : arg; if (typeof arg !== 'string') { diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index e3a95e87970de..b0fa44833e77e 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -14,8 +14,10 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { PLUGIN_RPC_CONTEXT, + AbstractPluginManagerExt, NotificationMain, MainMessageType, MessageRegistryMain, @@ -27,7 +29,8 @@ import { PluginManagerInitializeParams, PluginManagerStartParams, TerminalServiceExt, - LocalizationExt + LocalizationExt, + ExtensionKind } from '../common/plugin-api-rpc'; import { PluginMetadata, PluginJsonValidationContribution } from '../common/plugin-protocol'; import * as theia from '@theia/plugin'; @@ -35,13 +38,13 @@ import * as types from './types-impl'; import { join } from './path'; import { EnvExtImpl } from './env'; import { PreferenceRegistryExtImpl } from './preference-registry'; -import { Memento, KeyValueStorageProxy, GlobalState } from './plugin-storage'; +import { InternalStorageExt, Memento, GlobalState } from './plugin-storage'; import { ExtPluginApi } from '../common/plugin-ext-api-contribution'; import { RPCProtocol } from '../common/rpc-protocol'; -import { Emitter } from '@theia/core/lib/common/event'; +import { Emitter, Event } from '@theia/core/lib/common/event'; import { WebviewsExtImpl } from './webviews'; import { URI as Uri } from './types-impl'; -import { SecretsExtImpl, SecretStorageExt } from '../plugin/secrets-ext'; +import { InternalSecretsExt, SecretStorageExt } from '../plugin/secrets-ext'; import { PluginExt } from './plugin-context'; import { Deferred } from '@theia/core/lib/common/promise-util'; @@ -77,30 +80,34 @@ class ActivatedPlugin { } } -export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { - - static SUPPORTED_ACTIVATION_EVENTS = new Set([ - '*', - 'onLanguage', - 'onCommand', - 'onDebug', - 'onDebugInitialConfigurations', - 'onDebugResolve', - 'onDebugAdapterProtocolTracker', - 'onDebugDynamicConfigurations', - 'onTaskType', - 'workspaceContains', - 'onView', - 'onUri', - 'onTerminalProfile', - 'onWebviewPanel', - 'onFileSystem', - 'onCustomEditor', - 'onStartupFinished', - 'onAuthenticationRequest', - 'onNotebook', - 'onNotebookSerializer' - ]); +export const MinimalTerminalServiceExt = Symbol('MinimalTerminalServiceExt'); +export type MinimalTerminalServiceExt = Pick; + +@injectable() +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export abstract class AbstractPluginManagerExtImpl

    > implements AbstractPluginManagerExt

    , PluginManager { + + @inject(EnvExtImpl) + protected readonly envExt: EnvExtImpl; + + @inject(MinimalTerminalServiceExt) + protected readonly terminalService: MinimalTerminalServiceExt; + + @inject(InternalStorageExt) + protected readonly storage: InternalStorageExt; + + @inject(InternalSecretsExt) + protected readonly secrets: InternalSecretsExt; + + @inject(LocalizationExt) + protected readonly localization: LocalizationExt; + + @inject(RPCProtocol) + protected readonly rpc: RPCProtocol; + + // Cannot be Inversify-injected because it induces a dependency cycle + protected host: PluginHost; private configStorage: ConfigStorage | undefined; private readonly registry = new Map(); @@ -113,28 +120,23 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { private onDidChangeEmitter = new Emitter(); private messageRegistryProxy: MessageRegistryMain; private notificationMain: NotificationMain; - protected fireOnDidChange(): void { - this.onDidChangeEmitter.fire(undefined); - } protected jsonValidation: PluginJsonValidationContribution[] = []; + protected pluginKind = ExtensionKind.UI; protected ready = new Deferred(); - constructor( - private readonly host: PluginHost, - private readonly envExt: EnvExtImpl, - private readonly terminalService: TerminalServiceExt, - private readonly storageProxy: KeyValueStorageProxy, - private readonly secrets: SecretsExtImpl, - private readonly preferencesManager: PreferenceRegistryExtImpl, - private readonly webview: WebviewsExtImpl, - private readonly localization: LocalizationExt, - private readonly rpc: RPCProtocol - ) { + @postConstruct() + initialize(): void { this.messageRegistryProxy = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.MESSAGE_REGISTRY_MAIN); this.notificationMain = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTIFICATION_MAIN); } + setPluginHost(pluginHost: PluginHost): void { + this.host = pluginHost; + } + + abstract $init(params: P): Promise; + async $stop(pluginId?: string): Promise { if (!pluginId) { return this.stopAll(); @@ -201,26 +203,6 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { } } - async $init(params: PluginManagerInitializeParams): Promise { - this.storageProxy.init(params.globalState, params.workspaceState); - - this.envExt.setQueryParameters(params.env.queryParams); - this.envExt.setLanguage(params.env.language); - this.terminalService.$setShell(params.env.shell); - this.envExt.setUIKind(params.env.uiKind); - this.envExt.setApplicationName(params.env.appName); - this.envExt.setAppHost(params.env.appHost); - - this.preferencesManager.init(params.preferences); - - if (params.extApi) { - this.host.initExtApi(params.extApi); - } - - this.webview.init(params.webview); - this.jsonValidation = params.jsonValidation; - } - async $start(params: PluginManagerStartParams): Promise { this.configStorage = params.configStorage; @@ -259,22 +241,31 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { contributes.jsonValidation = (contributes.jsonValidation || []).concat(this.jsonValidation); } this.registry.set(plugin.model.id, plugin); - if (plugin.pluginPath && Array.isArray(plugin.rawModel.activationEvents)) { + const activationEvents = this.getActivationEvents(plugin); + if (plugin.pluginPath && activationEvents) { const activation = () => this.$activatePlugin(plugin.model.id); // an internal activation event is a subject to change this.setActivation(`onPlugin:${plugin.model.id}`, activation); - const unsupportedActivationEvents = plugin.rawModel.activationEvents.filter(e => !PluginManagerExtImpl.SUPPORTED_ACTIVATION_EVENTS.has(e.split(':')[0])); + const unsupportedActivationEvents = activationEvents.filter(e => !this.isSupportedActivationEvent(e)); if (unsupportedActivationEvents.length) { console.warn(`Unsupported activation events: ${unsupportedActivationEvents.join(', ')}, please open an issue: https://github.com/eclipse-theia/theia/issues/new`); } - for (let activationEvent of plugin.rawModel.activationEvents) { + for (let activationEvent of activationEvents) { if (activationEvent === 'onUri') { - activationEvent = `onUri:theia://${plugin.model.id}`; + activationEvent = `onUri:${this.envExt.uriScheme}://${plugin.model.id}`; } this.setActivation(activationEvent, activation); } } } + + protected getActivationEvents(plugin: Plugin): string[] | undefined { + const result = plugin.rawModel.activationEvents; + return Array.isArray(result) ? result : undefined; + } + + protected abstract isSupportedActivationEvent(activationEvent: string): boolean; + protected setActivation(activationEvent: string, activation: () => Promise): void { const activations = this.activations.get(activationEvent) || []; activations.push(activation); @@ -383,11 +374,12 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { const globalStoragePath = join(configStorage.hostGlobalStoragePath, plugin.model.id); const extension = new PluginExt(this, plugin); const extensionModeValue = plugin.isUnderDevelopment ? types.ExtensionMode.Development : types.ExtensionMode.Production; + const pluginContext: theia.PluginContext = { extensionPath: extension.extensionPath, extensionUri: extension.extensionUri, - globalState: new GlobalState(plugin.model.id, true, this.storageProxy), - workspaceState: new Memento(plugin.model.id, false, this.storageProxy), + globalState: new GlobalState(plugin.model.id, true, this.storage), + workspaceState: new Memento(plugin.model.id, false, this.storage), subscriptions: subscriptions, asAbsolutePath: asAbsolutePath, logPath: logPath, @@ -399,7 +391,14 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { environmentVariableCollection: this.terminalService.getEnvironmentVariableCollection(plugin.model.id), extensionMode: extensionModeValue, extension, - logUri: Uri.file(logPath) + logUri: Uri.file(logPath), + languageModelAccessInformation: { + /** @stubbed LanguageModelChat */ + onDidChange: (listener, thisArgs?, disposables?) => Event.None(listener, thisArgs, disposables), + canSendRequest(chat: theia.LanguageModelChat): boolean | undefined { + return undefined; + } + } }; this.pluginContextsMap.set(plugin.model.id, pluginContext); @@ -420,6 +419,10 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { } } + getPluginKind(): theia.ExtensionKind { + return this.pluginKind; + } + getAllPlugins(): Plugin[] { return Array.from(this.registry.values()); } @@ -451,6 +454,52 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { return this.onDidChangeEmitter.event; } + protected fireOnDidChange(): void { + this.onDidChangeEmitter.fire(undefined); + } + +} + +@injectable() +export class PluginManagerExtImpl extends AbstractPluginManagerExtImpl implements PluginManagerExt { + + @inject(PreferenceRegistryExtImpl) + protected readonly preferencesManager: PreferenceRegistryExtImpl; + + @inject(WebviewsExtImpl) + protected readonly webview: WebviewsExtImpl; + + private supportedActivationEvents: Set; + + async $init(params: PluginManagerInitializeParams): Promise { + this.storage.init(params.globalState, params.workspaceState); + + this.envExt.setQueryParameters(params.env.queryParams); + this.envExt.setUIKind(params.env.uiKind); + this.envExt.setLanguage(params.env.language); + this.terminalService.$setShell(params.env.shell); + this.envExt.setApplicationName(params.env.appName); + this.envExt.setAppHost(params.env.appHost); + this.envExt.setAppRoot(params.env.appRoot); + this.envExt.setAppUriScheme(params.env.appUriScheme); + + this.preferencesManager.init(params.preferences); + + if (params.extApi) { + this.host.initExtApi(params.extApi); + } + + this.webview.init(params.webview); + this.jsonValidation = params.jsonValidation; + this.pluginKind = params.pluginKind; + + this.supportedActivationEvents = new Set(params.supportedActivationEvents ?? []); + } + + protected isSupportedActivationEvent(activationEvent: string): boolean { + return this.supportedActivationEvents.has(activationEvent.split(':')[0]); + } + } // for electron diff --git a/packages/plugin-ext/src/plugin/plugin-storage.ts b/packages/plugin-ext/src/plugin/plugin-storage.ts index 9a9449ea44d76..eb2820c8d847e 100644 --- a/packages/plugin-ext/src/plugin/plugin-storage.ts +++ b/packages/plugin-ext/src/plugin/plugin-storage.ts @@ -15,7 +15,8 @@ // ***************************************************************************** import * as theia from '@theia/plugin'; -import { Event, Emitter } from '@theia/core/lib/common/event'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Disposable, DisposableGroup, Event, Emitter } from '@theia/core'; import { PLUGIN_RPC_CONTEXT, StorageMain, StorageExt } from '../common/plugin-api-rpc'; import { KeysToAnyValues, KeysToKeysToAnyValue } from '../common/types'; import { RPCProtocol } from '../common/rpc-protocol'; @@ -27,7 +28,7 @@ export class Memento implements theia.Memento { constructor( private readonly pluginId: string, private readonly isPluginGlobalData: boolean, - private readonly storage: KeyValueStorageProxy + private readonly storage: InternalStorageExt ) { this.cache = storage.getPerPluginData(pluginId, isPluginGlobalData); @@ -69,11 +70,28 @@ export class GlobalState extends Memento { setKeysForSync(keys: readonly string[]): void { } } +export const InternalStorageExt = Symbol('InternalStorageExt'); +export interface InternalStorageExt extends StorageExt { + + init(initGlobalData: KeysToKeysToAnyValue, initWorkspaceData: KeysToKeysToAnyValue): void; + + getPerPluginData(key: string, isGlobal: boolean): KeysToAnyValues; + + setPerPluginData(key: string, value: KeysToAnyValues, isGlobal: boolean): Promise; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + storageDataChangedEvent(listener: (e: KeysToKeysToAnyValue) => any, thisArgs?: any, disposables?: DisposableGroup): Disposable; + + $updatePluginsWorkspaceData(workspaceData: KeysToKeysToAnyValue): void; + +} + /** * Singleton. * Is used to proxy storage requests to main side. */ -export class KeyValueStorageProxy implements StorageExt { +@injectable() +export class KeyValueStorageProxy implements InternalStorageExt { private storageDataChangedEmitter = new Emitter(); public readonly storageDataChangedEvent: Event = this.storageDataChangedEmitter.event; @@ -83,7 +101,7 @@ export class KeyValueStorageProxy implements StorageExt { private globalDataCache: KeysToKeysToAnyValue; private workspaceDataCache: KeysToKeysToAnyValue; - constructor(rpc: RPCProtocol) { + constructor(@inject(RPCProtocol) rpc: RPCProtocol) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.STORAGE_MAIN); } diff --git a/packages/plugin-ext/src/plugin/preference-registry.spec.ts b/packages/plugin-ext/src/plugin/preference-registry.spec.ts index 8874ccfff41fd..aaba6b4c6f97f 100644 --- a/packages/plugin-ext/src/plugin/preference-registry.spec.ts +++ b/packages/plugin-ext/src/plugin/preference-registry.spec.ts @@ -14,6 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { Container } from '@theia/core/shared/inversify'; import { PreferenceRegistryExtImpl, PreferenceScope } from './preference-registry'; import * as chai from 'chai'; import { WorkspaceExtImpl } from '../plugin/workspace'; @@ -38,7 +39,11 @@ describe('PreferenceRegistryExtImpl:', () => { const mockWorkspace: WorkspaceExtImpl = { workspaceFolders: [{ uri: workspaceRoot, name: 'workspace-root', index: 0 }] } as WorkspaceExtImpl; beforeEach(() => { - preferenceRegistryExtImpl = new PreferenceRegistryExtImpl(mockRPC, mockWorkspace); + const container = new Container(); + container.bind(RPCProtocol).toConstantValue(mockRPC); + container.bind(WorkspaceExtImpl).toConstantValue(mockWorkspace); + container.bind(PreferenceRegistryExtImpl).toSelf().inSingletonScope(); + preferenceRegistryExtImpl = container.get(PreferenceRegistryExtImpl); }); describe('Prototype pollution', () => { diff --git a/packages/plugin-ext/src/plugin/preference-registry.ts b/packages/plugin-ext/src/plugin/preference-registry.ts index 465c7122ec4af..6d676309d1889 100644 --- a/packages/plugin-ext/src/plugin/preference-registry.ts +++ b/packages/plugin-ext/src/plugin/preference-registry.ts @@ -16,6 +16,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { isOSX, isWindows } from '@theia/core/lib/common/os'; import { URI } from '@theia/core/shared/vscode-uri'; @@ -24,7 +25,7 @@ import { IConfigurationOverrides } from '@theia/monaco-editor-core/esm/vs/platfo import { Configuration, ConfigurationModel, ConfigurationModelParser } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configurationModels'; import { Workspace, WorkspaceFolder } from '@theia/monaco-editor-core/esm/vs/platform/workspace/common/workspace'; import * as theia from '@theia/plugin'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import { PLUGIN_RPC_CONTEXT, PreferenceChangeExt, PreferenceData, PreferenceRegistryExt, PreferenceRegistryMain @@ -74,22 +75,27 @@ function lookUp(tree: any, key: string): any { export class TheiaWorkspace extends Workspace { constructor(ext: WorkspaceExtImpl) { const folders = (ext.workspaceFolders ?? []).map(folder => new WorkspaceFolder(folder)); - super(v4(), folders, false, ext.workspaceFile ?? null, () => isOSX || isWindows); + super(generateUuid(), folders, false, ext.workspaceFile ?? null, () => isOSX || isWindows); } } +@injectable() export class PreferenceRegistryExtImpl implements PreferenceRegistryExt { + @inject(RPCProtocol) + protected rpc: RPCProtocol; + + @inject(WorkspaceExtImpl) + protected readonly workspace: WorkspaceExtImpl; + private proxy: PreferenceRegistryMain; private _preferences: Configuration; private readonly _onDidChangeConfiguration = new Emitter(); readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; - constructor( - rpc: RPCProtocol, - private readonly workspace: WorkspaceExtImpl - ) { - this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.PREFERENCE_REGISTRY_MAIN); + @postConstruct() + initialize(): void { + this.proxy = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.PREFERENCE_REGISTRY_MAIN); } init(data: PreferenceData): void { @@ -192,10 +198,10 @@ export class PreferenceRegistryExtImpl implements PreferenceRegistryExt { } const configInspect: ConfigurationInspect = { key }; - configInspect.defaultValue = result.default?.value; - configInspect.globalValue = result.user?.value; - configInspect.workspaceValue = result.workspace?.value; - configInspect.workspaceFolderValue = result.workspaceFolder?.value; + configInspect.defaultValue = cloneDeep(result.default?.value); + configInspect.globalValue = cloneDeep(result.user?.value); + configInspect.workspaceValue = cloneDeep(result.workspace?.value); + configInspect.workspaceFolderValue = cloneDeep(result.workspaceFolder?.value); return configInspect; } }; diff --git a/packages/plugin-ext/src/plugin/quick-open.ts b/packages/plugin-ext/src/plugin/quick-open.ts index 82a564107a404..c9f51aa6c3f60 100644 --- a/packages/plugin-ext/src/plugin/quick-open.ts +++ b/packages/plugin-ext/src/plugin/quick-open.ts @@ -16,7 +16,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { QuickOpenExt, PLUGIN_RPC_CONTEXT as Ext, QuickOpenMain, TransferInputBox, Plugin, - Item, TransferQuickInputButton, TransferQuickPickItems, TransferQuickInput + TransferQuickInputButton, TransferQuickInput, TransferQuickPickItem } from '../common/plugin-api-rpc'; import * as theia from '@theia/plugin'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; @@ -30,8 +30,8 @@ import { convertToTransferQuickPickItems } from './type-converters'; import { PluginPackage } from '../common/plugin-protocol'; import { QuickInputButtonHandle } from '@theia/core/lib/browser'; import { MaybePromise } from '@theia/core/lib/common/types'; -import Severity from '@theia/monaco-editor-core/esm/vs/base/common/severity'; -import { ThemeIcon as MonacoThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { Severity } from '@theia/core/lib/common/severity'; +import { PluginIconPath } from './plugin-icon-path'; const canceledName = 'Canceled'; /** @@ -42,42 +42,7 @@ export function isPromiseCanceledError(error: any): boolean { return error instanceof Error && error.name === canceledName && error.message === canceledName; } -export function getIconUris(iconPath: theia.QuickInputButton['iconPath']): { dark: URI, light: URI } | { id: string } { - if (ThemeIcon.is(iconPath)) { - return { id: iconPath.id }; - } - const dark = getDarkIconUri(iconPath as URI | { light: URI; dark: URI; }); - const light = getLightIconUri(iconPath as URI | { light: URI; dark: URI; }); - // Tolerate strings: https://github.com/microsoft/vscode/issues/110432#issuecomment-726144556 - return { - dark: typeof dark === 'string' ? URI.file(dark) : dark, - light: typeof light === 'string' ? URI.file(light) : light - }; -} - -export function getLightIconUri(iconPath: URI | { light: URI; dark: URI; }): URI { - return typeof iconPath === 'object' && 'light' in iconPath ? iconPath.light : iconPath; -} - -export function getDarkIconUri(iconPath: URI | { light: URI; dark: URI; }): URI { - return typeof iconPath === 'object' && 'dark' in iconPath ? iconPath.dark : iconPath; -} - -export function getIconPathOrClass(button: theia.QuickInputButton): { iconPath: { dark: URI; light?: URI | undefined; } | undefined; iconClass: string | undefined; } { - const iconPathOrIconClass = getIconUris(button.iconPath); - let iconPath: { dark: URI; light?: URI | undefined } | undefined; - let iconClass: string | undefined; - if ('id' in iconPathOrIconClass) { - iconClass = MonacoThemeIcon.asClassName(iconPathOrIconClass); - } else { - iconPath = iconPathOrIconClass; - } - - return { - iconPath, - iconClass - }; -} +type Item = theia.QuickPickItem | string; export class QuickOpenExtImpl implements QuickOpenExt { private proxy: QuickOpenMain; @@ -91,13 +56,13 @@ export class QuickOpenExtImpl implements QuickOpenExt { } /* eslint-disable max-len */ - showQuickPick(itemsOrItemsPromise: Array | Promise>, options: theia.QuickPickOptions & { canPickMany: true; }, token?: theia.CancellationToken): Promise | undefined>; - showQuickPick(itemsOrItemsPromise: string[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: Array | Promise>, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: Item[] | Promise, options?: theia.QuickPickOptions, token: theia.CancellationToken = CancellationToken.None): Promise { + showQuickPick(plugin: Plugin, itemsOrItemsPromise: theia.QuickPickItem[] | Promise, options: theia.QuickPickOptions & { canPickMany: true; }, token?: theia.CancellationToken): Promise | undefined>; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: string[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: theia.QuickPickItem[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: Item[] | Promise, options?: theia.QuickPickOptions, token: theia.CancellationToken = CancellationToken.None): Promise { this.onDidSelectItem = undefined; - const itemsPromise = >Promise.resolve(itemsOrItemsPromise); + const itemsPromise = Promise.resolve(itemsOrItemsPromise); const instance = ++this._instances; @@ -118,7 +83,7 @@ export class QuickOpenExtImpl implements QuickOpenExt { return undefined; } return itemsPromise.then(async items => { - const pickItems: Array = convertToTransferQuickPickItems(items); + const pickItems = convertToTransferQuickPickItems(plugin, items); if (options && typeof options.onDidSelectItem === 'function') { this.onDidSelectItem = handle => { @@ -246,11 +211,11 @@ export class QuickOpenExtImpl implements QuickOpenExt { async $acceptOnDidTriggerButton(sessionId: number, btn: QuickInputButtonHandle): Promise { const session = this._sessions.get(sessionId); if (session) { - if (btn.index === -1) { + if (btn.handle === -1) { session._fireButtonTrigger(QuickInputButtons.Back); } else if (session && (session instanceof InputBoxExt || session instanceof QuickPickExt)) { - const btnFromIndex = session.buttons[btn.index]; - session._fireButtonTrigger(btnFromIndex as theia.QuickInputButton); + const btnFromHandle = session.buttons[btn.handle]; + session._fireButtonTrigger(btnFromHandle as theia.QuickInputButton); } } } @@ -412,8 +377,7 @@ export class QuickInputExt implements theia.QuickInput { }); this.update({ buttons: buttons.map((button, i) => ({ - iconPath: getIconUris(button.iconPath), - iconClass: ThemeIcon.is(button.iconPath) ? MonacoThemeIcon.asClassName(button.iconPath) : undefined, + iconUrl: PluginIconPath.toUrl(button.iconPath, this.plugin) ?? ThemeIcon.get(button.iconPath), tooltip: button.tooltip, handle: button === QuickInputButtons.Back ? -1 : i, })) @@ -469,7 +433,6 @@ export class QuickInputExt implements theia.QuickInput { hide(): void { this.quickOpenMain.$hide(); - this.dispose(); } protected convertURL(iconPath: URI | { light: string | URI; dark: string | URI } | ThemeIcon): @@ -646,24 +609,23 @@ export class QuickPickExt extends QuickInputExt i this._itemsToHandles.set(item, i); }); - const pickItems: TransferQuickPickItems[] = []; + const pickItems: TransferQuickPickItem[] = []; for (let handle = 0; handle < items.length; handle++) { const item = items[handle]; if (item.kind === QuickPickItemKind.Separator) { - pickItems.push({ type: 'separator', label: item.label, handle }); + pickItems.push({ kind: 'separator', label: item.label, handle }); } else { pickItems.push({ - kind: item.kind, + kind: 'item', label: item.label, - iconPath: item.iconPath ? getIconUris(item.iconPath) : undefined, + iconUrl: PluginIconPath.toUrl(item.iconPath, this.plugin) ?? ThemeIcon.get(item.iconPath), description: item.description, handle, detail: item.detail, picked: item.picked, alwaysShow: item.alwaysShow, buttons: item.buttons?.map((button, index) => ({ - iconPath: getIconUris(button.iconPath), - iconClass: ThemeIcon.is(button.iconPath) ? MonacoThemeIcon.asClassName(button.iconPath) : undefined, + iconUrl: PluginIconPath.toUrl(button.iconPath, this.plugin) ?? ThemeIcon.get(button.iconPath), tooltip: button.tooltip, handle: button === QuickInputButtons.Back ? -1 : index, })) diff --git a/packages/plugin-ext/src/plugin/scm.ts b/packages/plugin-ext/src/plugin/scm.ts index 4703ab3626f14..f34556cee2ae8 100644 --- a/packages/plugin-ext/src/plugin/scm.ts +++ b/packages/plugin-ext/src/plugin/scm.ts @@ -39,6 +39,7 @@ import { URI, ThemeIcon } from './types-impl'; import { ScmCommandArg } from '../common/plugin-api-rpc'; import { sep } from '@theia/core/lib/common/paths'; import { PluginIconPath } from './plugin-icon-path'; +import { createAPIObject } from './plugin-context'; type ProviderHandle = number; type GroupHandle = number; type ResourceStateHandle = number; @@ -290,6 +291,7 @@ interface ValidateInput { export class ScmInputBoxImpl implements theia.SourceControlInputBox { private _value: string = ''; + apiObject: theia.SourceControlInputBox; get value(): string { return this._value; @@ -354,7 +356,7 @@ export class ScmInputBoxImpl implements theia.SourceControlInputBox { } constructor(private plugin: Plugin, private proxy: ScmMain, private sourceControlHandle: number) { - // noop + this.apiObject = createAPIObject(this); } onInputBoxValueChange(value: string): void { @@ -543,8 +545,7 @@ class SourceControlImpl implements theia.SourceControl { return this._rootUri; } - private _inputBox: ScmInputBoxImpl; - get inputBox(): ScmInputBoxImpl { return this._inputBox; } + readonly inputBox: ScmInputBoxImpl; private _count: number | undefined = undefined; @@ -642,7 +643,7 @@ class SourceControlImpl implements theia.SourceControl { private _label: string, private _rootUri?: theia.Uri ) { - this._inputBox = new ScmInputBoxImpl(plugin, this.proxy, this.handle); + this.inputBox = new ScmInputBoxImpl(plugin, this.proxy, this.handle); this.proxy.$registerSourceControl(this.handle, _id, _label, _rootUri); } diff --git a/packages/plugin-ext/src/plugin/secrets-ext.ts b/packages/plugin-ext/src/plugin/secrets-ext.ts index 5bd9a18bbabcf..acfaba814b07c 100644 --- a/packages/plugin-ext/src/plugin/secrets-ext.ts +++ b/packages/plugin-ext/src/plugin/secrets-ext.ts @@ -20,17 +20,37 @@ *--------------------------------------------------------------------------------------------*/ // code copied and modified from https://github.com/microsoft/vscode/blob/1.55.2/src/vs/workbench/api/common/extHostSecrets.ts +import { inject, injectable } from '@theia/core/shared/inversify'; import { Plugin, PLUGIN_RPC_CONTEXT, SecretsExt, SecretsMain } from '../common/plugin-api-rpc'; import { RPCProtocol } from '../common/rpc-protocol'; import { Event, Emitter } from '@theia/core/lib/common/event'; +import { Disposable, DisposableGroup } from '@theia/core'; import * as theia from '@theia/plugin'; -export class SecretsExtImpl implements SecretsExt { +export interface PasswordChange { + extensionId: string; + key: string; +} + +export const InternalSecretsExt = Symbol('InternalSecretsExt'); +export interface InternalSecretsExt extends SecretsExt { + get(extensionId: string, key: string): Promise; + + store(extensionId: string, key: string, value: string): Promise; + + delete(extensionId: string, key: string): Promise; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onDidChangePassword(listener: (e: PasswordChange) => any, thisArgs?: any, disposables?: DisposableGroup): Disposable; +} + +@injectable() +export class SecretsExtImpl implements InternalSecretsExt { private proxy: SecretsMain; - private onDidChangePasswordEmitter = new Emitter<{ extensionId: string, key: string }>(); + private onDidChangePasswordEmitter = new Emitter(); readonly onDidChangePassword = this.onDidChangePasswordEmitter.event; - constructor(rpc: RPCProtocol) { + constructor(@inject(RPCProtocol) rpc: RPCProtocol) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.SECRETS_MAIN); } @@ -54,12 +74,12 @@ export class SecretsExtImpl implements SecretsExt { export class SecretStorageExt implements theia.SecretStorage { protected readonly id: string; - readonly secretState: SecretsExtImpl; + readonly secretState: InternalSecretsExt; private onDidChangeEmitter = new Emitter(); readonly onDidChange: Event = this.onDidChangeEmitter.event; - constructor(pluginDescription: Plugin, secretState: SecretsExtImpl) { + constructor(pluginDescription: Plugin, secretState: InternalSecretsExt) { this.id = pluginDescription.model.id.toLowerCase(); this.secretState = secretState; diff --git a/packages/plugin-ext/src/plugin/tabs.ts b/packages/plugin-ext/src/plugin/tabs.ts index a1a4761632371..80a263b778bf3 100644 --- a/packages/plugin-ext/src/plugin/tabs.ts +++ b/packages/plugin-ext/src/plugin/tabs.ts @@ -325,8 +325,13 @@ export class TabsExtImpl implements TabsExt { this.activeGroupId = activeTabGroupId; } } - this.onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed })); - this.onDidChangeTabs.fire({ opened: tabsOpened, changed: [], closed: [] }); + + if (closed.length > 0 || opened.length > 0 || changed.length > 0) { + this.onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed })); + } + if (tabsOpened.length > 0) { + this.onDidChangeTabs.fire({ opened: tabsOpened, changed: [], closed: [] }); + } } $acceptTabGroupUpdate(groupDto: TabGroupDto): void { diff --git a/packages/plugin-ext/src/plugin/terminal-ext.ts b/packages/plugin-ext/src/plugin/terminal-ext.ts index 7c158fd36a8cb..ba849bc9fb305 100644 --- a/packages/plugin-ext/src/plugin/terminal-ext.ts +++ b/packages/plugin-ext/src/plugin/terminal-ext.ts @@ -13,9 +13,10 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** + import { UUID } from '@theia/core/shared/@phosphor/coreutils'; -import { Terminal, TerminalOptions, PseudoTerminalOptions, ExtensionTerminalOptions, TerminalState } from '@theia/plugin'; -import { TerminalServiceExt, TerminalServiceMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { TerminalServiceExt, TerminalServiceMain, PLUGIN_RPC_CONTEXT, Plugin, TerminalOptions } from '../common/plugin-api-rpc'; import { RPCProtocol } from '../common/rpc-protocol'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { MultiKeyMap } from '@theia/core/lib/common/collections'; @@ -25,29 +26,14 @@ import * as Converter from './type-converters'; import { Disposable, EnvironmentVariableMutatorType, TerminalExitReason, ThemeIcon } from './types-impl'; import { NO_ROOT_URI, SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/shell-terminal-protocol'; import { ProvidedTerminalLink } from '../common/plugin-api-rpc-model'; -import { ThemeIcon as MonacoThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; - -export function getIconUris(iconPath: theia.TerminalOptions['iconPath']): { id: string } | undefined { - if (ThemeIcon.is(iconPath)) { - return { id: iconPath.id }; - } - return undefined; -} - -export function getIconClass(options: theia.TerminalOptions | theia.ExtensionTerminalOptions): string | undefined { - const iconClass = getIconUris(options.iconPath); - if (iconClass) { - return MonacoThemeIcon.asClassName(iconClass); - } - return undefined; -} +import { PluginIconPath } from './plugin-icon-path'; /** * Provides high level terminal plugin api to use in the Theia plugins. * This service allow(with help proxy) create and use terminal emulator. */ +@injectable() export class TerminalServiceExtImpl implements TerminalServiceExt { - private readonly proxy: TerminalServiceMain; private readonly _terminals = new Map(); @@ -56,18 +42,19 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { private static nextProviderId = 0; private readonly terminalLinkProviders = new Map(); + private readonly terminalObservers = new Map(); private readonly terminalProfileProviders = new Map(); - private readonly onDidCloseTerminalEmitter = new Emitter(); - readonly onDidCloseTerminal: theia.Event = this.onDidCloseTerminalEmitter.event; + private readonly onDidCloseTerminalEmitter = new Emitter(); + readonly onDidCloseTerminal: theia.Event = this.onDidCloseTerminalEmitter.event; - private readonly onDidOpenTerminalEmitter = new Emitter(); - readonly onDidOpenTerminal: theia.Event = this.onDidOpenTerminalEmitter.event; + private readonly onDidOpenTerminalEmitter = new Emitter(); + readonly onDidOpenTerminal: theia.Event = this.onDidOpenTerminalEmitter.event; - private readonly onDidChangeActiveTerminalEmitter = new Emitter(); - readonly onDidChangeActiveTerminal: theia.Event = this.onDidChangeActiveTerminalEmitter.event; + private readonly onDidChangeActiveTerminalEmitter = new Emitter(); + readonly onDidChangeActiveTerminal: theia.Event = this.onDidChangeActiveTerminalEmitter.event; - private readonly onDidChangeTerminalStateEmitter = new Emitter(); - readonly onDidChangeTerminalState: theia.Event = this.onDidChangeTerminalStateEmitter.event; + private readonly onDidChangeTerminalStateEmitter = new Emitter(); + readonly onDidChangeTerminalState: theia.Event = this.onDidChangeTerminalStateEmitter.event; protected environmentVariableCollections: MultiKeyMap = new MultiKeyMap(2); @@ -75,7 +62,7 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { private readonly onDidChangeShellEmitter = new Emitter(); readonly onDidChangeShell: theia.Event = this.onDidChangeShellEmitter.event; - constructor(rpc: RPCProtocol) { + constructor(@inject(RPCProtocol) rpc: RPCProtocol) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TERMINAL_MAIN); } @@ -95,9 +82,10 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { } createTerminal( - nameOrOptions: TerminalOptions | PseudoTerminalOptions | ExtensionTerminalOptions | (string | undefined), + plugin: Plugin, + nameOrOptions: theia.TerminalOptions | theia.PseudoTerminalOptions | theia.ExtensionTerminalOptions | string | undefined, shellPath?: string, shellArgs?: string[] | string - ): Terminal { + ): theia.Terminal { const id = `plugin-terminal-${UUID.uuid4()}`; let options: TerminalOptions; let pseudoTerminal: theia.Pseudoterminal | undefined = undefined; @@ -120,7 +108,6 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { } let parentId; - if (options.location && typeof options.location === 'object' && 'parentTerminal' in options.location) { const parentTerminal = options.location.parentTerminal; if (parentTerminal instanceof TerminalExtImpl) { @@ -133,6 +120,15 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { } } + if (typeof nameOrOptions === 'object' && 'iconPath' in nameOrOptions) { + const iconPath = nameOrOptions.iconPath; + options.iconUrl = PluginIconPath.toUrl(iconPath, plugin) ?? ThemeIcon.get(iconPath); + } + + if (typeof nameOrOptions === 'object' && 'color' in nameOrOptions) { + options.color = nameOrOptions.color; + } + this.proxy.$createTerminal(id, options, parentId, !!pseudoTerminal); let creationOptions: theia.TerminalOptions | theia.ExtensionTerminalOptions = options; @@ -268,6 +264,25 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { return Disposable.NULL; } + registerTerminalObserver(observer: theia.TerminalObserver): theia.Disposable { + const id = (TerminalServiceExtImpl.nextProviderId++).toString(); + this.terminalObservers.set(id, observer); + this.proxy.$registerTerminalObserver(id, observer.nrOfLinesToMatch, observer.outputMatcherRegex); + return Disposable.create(() => { + this.proxy.$unregisterTerminalObserver(id); + this.terminalObservers.delete(id); + }); + } + + $reportOutputMatch(observerId: string, groups: string[]): void { + const observer = this.terminalObservers.get(observerId); + if (observer) { + observer.matchOccurred(groups); + } else { + throw new Error(`reporting matches for unregistered observer: ${observerId} `); + } + } + protected isExtensionTerminalOptions(options: theia.TerminalOptions | theia.ExtensionTerminalOptions): options is theia.ExtensionTerminalOptions { return 'pty' in options; } @@ -441,7 +456,7 @@ export class EnvironmentVariableCollectionImpl implements theia.GlobalEnvironmen } } -export class TerminalExtImpl implements Terminal { +export class TerminalExtImpl implements theia.Terminal { name: string; @@ -455,16 +470,19 @@ export class TerminalExtImpl implements Terminal { return this.deferredProcessId.promise; } - readonly creationOptions: Readonly; + readonly creationOptions: Readonly; - state: TerminalState = { isInteractedWith: false }; + state: theia.TerminalState = { isInteractedWith: false }; constructor(private readonly proxy: TerminalServiceMain, private readonly options: theia.TerminalOptions | theia.ExtensionTerminalOptions) { this.creationOptions = this.options; } - sendText(text: string, addNewLine: boolean = true): void { - this.id.promise.then(id => this.proxy.$sendText(id, text, addNewLine)); + /** @stubbed Terminal Shell Ingration */ + shellIntegration: theia.TerminalShellIntegration | undefined = undefined; + + sendText(text: string, shouldExecute: boolean = true): void { + this.id.promise.then(id => this.proxy.$sendText(id, text, shouldExecute)); } show(preserveFocus?: boolean): void { diff --git a/packages/plugin-ext/src/plugin/tests.ts b/packages/plugin-ext/src/plugin/tests.ts index e58bc432d91da..514bcbbe4fabc 100644 --- a/packages/plugin-ext/src/plugin/tests.ts +++ b/packages/plugin-ext/src/plugin/tests.ts @@ -34,16 +34,19 @@ import { isDefined } from '@theia/core/lib/common/types'; import { TestingExt, PLUGIN_RPC_CONTEXT, TestingMain } from '../common/plugin-api-rpc'; import { CommandRegistryImpl } from './command-registry'; import { RPCProtocol } from '../common/rpc-protocol'; -import { v4 as uuidv4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import * as Convert from './type-converters'; import { TestItemImpl, TestItemCollection } from './test-item'; import { AccumulatingTreeDeltaEmitter, TreeDelta } from '@theia/test/lib/common/tree-delta'; import { TestItemDTO, TestOutputDTO, TestExecutionState, TestRunProfileDTO, - TestRunProfileKind, TestRunRequestDTO, TestStateChangeDTO, TestItemReference + TestRunProfileKind, TestRunRequestDTO, TestStateChangeDTO, TestItemReference, TestMessageArg, TestMessageDTO, + TestMessageStackFrameDTO } from '../common/test-types'; +import * as protocol from '@theia/core/shared/vscode-languageserver-protocol'; import { ChangeBatcher, observableProperty } from '@theia/test/lib/common/collections'; -import { TestRunRequest } from './types-impl'; +import { Location, Position, Range, TestRunRequest, URI } from './types-impl'; +import { MarkdownString } from '../common/plugin-api-rpc-model'; type RefreshHandler = (token: theia.CancellationToken) => void | theia.Thenable; type ResolveHandler = (item: theia.TestItem | undefined) => theia.Thenable | void; @@ -134,21 +137,7 @@ export class TestControllerImpl implements theia.TestController { } createTestRun(request: theia.TestRunRequest, name?: string, persist: boolean = true): theia.TestRun { - return this.testRunStarted(request, name || '', persist, true); - } - - dispose() { - this.proxy.$unregisterTestController(this.id); - this.onDispose(); - } - - protected testRunStarted(request: theia.TestRunRequest, name: string, persist: boolean, isRunning: boolean): TestRun { - const existing = this.activeRuns.get(request); - if (existing) { - return existing; - } - - const run = new TestRun(this, this.proxy, name, persist, isRunning); + const run = new TestRun(this, this.proxy, name || '', persist, true, request.preserveFocus); const endListener = run.onWillFlush(() => { // make sure we notify the front end of test item changes before test run state is sent this.deltaBuilder.flush(); @@ -161,7 +150,12 @@ export class TestControllerImpl implements theia.TestController { return run; } - runTestsForUI(profileId: string, name: string, includedTests: string[][], excludedTests: string[][]): void { + dispose() { + this.proxy.$unregisterTestController(this.id); + this.onDispose(); + } + + runTestsForUI(profileId: string, name: string, includedTests: string[][], excludedTests: string[][], preserveFocus: boolean): void { const profile = this.getProfile(profileId); if (!profile) { console.error(`No test run profile found for controller ${this.id} with id ${profileId} `); @@ -196,11 +190,11 @@ export class TestControllerImpl implements theia.TestController { .filter(isDefined); const request = new TestRunRequest( - includeTests, excludeTests, profile, false // don't support continuous run yet + includeTests, excludeTests, profile, false /* don't support continuous run yet */, preserveFocus ); - const run = this.testRunStarted(request, name, false, false); - profile.runHandler(request, run.token); + // we do not cancel test runs via a cancellation token, but instead invoke "cancel" on the test runs + profile.runHandler(request, CancellationToken.None); } cancelRun(runId?: string): void { @@ -234,11 +228,24 @@ function checkTestInstance(item?: theia.TestItem): TestItemImpl | undefined { return undefined; } -class TestRun implements theia.TestRun { +export function checkTestRunInstance(item: theia.TestRun): TestRun; +export function checkTestRunInstance(item?: theia.TestRun): TestRun | undefined; +export function checkTestRunInstance(item?: theia.TestRun): TestRun | undefined { + if (item instanceof TestRun) { + return item; + } else if (item) { + throw new Error('Not a TestRun instance'); + } + return undefined; +} + +export class TestRun implements theia.TestRun { private onDidEndEmitter = new Emitter(); onDidEnd: Event = this.onDidEndEmitter.event; private onWillFlushEmitter = new Emitter(); onWillFlush: Event = this.onWillFlushEmitter.event; + private onDidDisposeEmitter = new Emitter(); + onDidDispose: Event = this.onDidDisposeEmitter.event; readonly id: string; private testStateDeltas = new Map(); @@ -248,21 +255,25 @@ class TestRun implements theia.TestRun { }, 200); private ended: boolean; private tokenSource: CancellationTokenSource; - readonly token: CancellationToken; constructor( - private readonly controller: TestControllerImpl, + readonly controller: TestControllerImpl, private readonly proxy: TestingMain, readonly name: string, readonly isPersisted: boolean, - isRunning: boolean) { - this.id = uuidv4(); + isRunning: boolean, + preserveFocus: boolean) { + this.id = generateUuid(); this.tokenSource = new CancellationTokenSource(); - this.token = this.tokenSource.token; - this.proxy.$notifyTestRunCreated(this.controller.id, { id: this.id, name: this.name, isRunning }); + this.proxy.$notifyTestRunCreated(this.controller.id, { id: this.id, name: this.name, isRunning }, preserveFocus); + } + + get token(): CancellationToken { + return this.tokenSource.token; } + enqueued(test: theia.TestItem): void { this.updateTestState(test, { itemPath: checkTestInstance(test).path, state: TestExecutionState.Queued }); } @@ -292,6 +303,9 @@ class TestRun implements theia.TestRun { this.proxy.$notifyTestRunEnded(this.controller.id, this.id); } + /** @stubbed */ + addCoverage(fileCoverage: theia.FileCoverage): void { } + private checkNotEnded(test: theia.TestItem): boolean { if (this.ended) { console.warn(`Setting the state of test "${test.id}" is a no - op after the run ends.`); @@ -335,6 +349,8 @@ export class TestingExtImpl implements TestingExt { return this.toTestItem(arg); } else if (Array.isArray(arg)) { return arg.map(param => TestItemReference.is(param) ? this.toTestItem(param) : param); + } else if (TestMessageArg.is(arg)) { + return this.fromTestMessageArg(arg); } else { return arg; } @@ -343,6 +359,56 @@ export class TestingExtImpl implements TestingExt { } + fromTestMessageArg(arg: TestMessageArg): { test?: theia.TestItem, message: theia.TestMessage } { + const testItem = arg.testItemReference ? this.toTestItem(arg.testItemReference) : undefined; + const message = this.toTestMessage(arg.testMessage); + return { + test: testItem, + message: message + }; + } + + toTestMessage(testMessage: TestMessageDTO): theia.TestMessage { + const message = MarkdownString.is(testMessage.message) ? Convert.toMarkdown(testMessage.message) : testMessage.message; + + return { + message: message, + actualOutput: testMessage.actual, + expectedOutput: testMessage.expected, + contextValue: testMessage.contextValue, + location: this.toLocation(testMessage.location), + stackTrace: testMessage.stackTrace ? testMessage.stackTrace.map(frame => this.toStackFrame(frame)) : undefined + }; + } + + toLocation(location: protocol.Location | undefined): Location | undefined { + if (!location) { + return undefined; + } + return new Location(URI.parse(location.uri), this.toRange(location.range)); + } + + toRange(range: protocol.Range): Range { + return new Range(this.toPosition(range.start), this.toPosition(range.end)); + } + + toPosition(position: protocol.Position): Position; + toPosition(position: protocol.Position | undefined): Position | undefined; + toPosition(position: protocol.Position | undefined): Position | undefined { + if (!position) { + return undefined; + } + return new Position(position.line, position.character); + } + + toStackFrame(stackFrame: TestMessageStackFrameDTO): theia.TestMessageStackFrame { + return { + label: stackFrame.label, + position: this.toPosition(stackFrame.position), + uri: stackFrame.uri ? URI.parse(stackFrame.uri) : undefined + }; + } + toTestItem(ref: TestItemReference): theia.TestItem { const result = this.withController(ref.controllerId).items.find(ref.testPath); if (!result) { @@ -392,6 +458,14 @@ export class TestingExtImpl implements TestingExt { this.controllersById.get(controllerId)?.getProfile(profileId)?.configureHandler?.(); } + /** @inheritdoc */ + $onDidChangeDefault(controllerId: string, profileId: string, isDefault: boolean): void { + const profile = this.controllersById.get(controllerId)?.getProfile(profileId); + if (profile) { + profile.doSetDefault(isDefault); + } + } + /** @inheritdoc */ async $refreshTests(controllerId: string, token: CancellationToken): Promise { await this.withController(controllerId).refreshHandler?.(token); @@ -407,7 +481,7 @@ export class TestingExtImpl implements TestingExt { } runTestsForUI(req: TestRunRequestDTO): void { - this.withController(req.controllerId).runTestsForUI(req.profileId, req.name, req.includedTests, req.excludedTests); + this.withController(req.controllerId).runTestsForUI(req.profileId, req.name, req.includedTests, req.excludedTests, req.preserveFocus); } /** @@ -432,13 +506,7 @@ export class TestRunProfile implements theia.TestRunProfile { isDefault = false, tag: theia.TestTag | undefined = undefined, ) { - this.proxy = proxy; - this.label = label; - this.tag = tag; - this.label = label; - this.isDefault = isDefault; - - this.proxy.$notifyTestRunProfileCreated(controllerId, { + proxy.$notifyTestRunProfileCreated(controllerId, { id: profileId, kind: kind, tag: tag ? tag.toString() : '', @@ -446,6 +514,11 @@ export class TestRunProfile implements theia.TestRunProfile { isDefault: isDefault, canConfigure: false, }); + this.proxy = proxy; + this.label = label; + this.tag = tag; + this.label = label; + this.isDefault = isDefault; } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -459,8 +532,29 @@ export class TestRunProfile implements theia.TestRunProfile { @observableProperty('notifyPropertyChange') label: string; - @observableProperty('notifyPropertyChange') - isDefault: boolean; + _isDefault: boolean; + + get isDefault(): boolean { + return this._isDefault; + } + + set isDefault(isDefault: boolean) { + if (this.doSetDefault(isDefault)) { + this.proxy.$updateTestRunProfile(this.controllerId, this.profileId, { isDefault: isDefault }); + } + } + + doSetDefault(isDefault: boolean): boolean { + if (this._isDefault !== isDefault) { + this._isDefault = isDefault; + this.onDidChangeDefaultEmitter.fire(isDefault); + return true; + } + return false; + } + + private onDidChangeDefaultEmitter = new Emitter(); + onDidChangeDefault = this.onDidChangeDefaultEmitter.event; @observableProperty('notifyTagChange') tag: theia.TestTag | undefined; diff --git a/packages/plugin-ext/src/plugin/text-editor.ts b/packages/plugin-ext/src/plugin/text-editor.ts index 85b87747107ba..98f578f3b567a 100644 --- a/packages/plugin-ext/src/plugin/text-editor.ts +++ b/packages/plugin-ext/src/plugin/text-editor.ts @@ -30,7 +30,7 @@ export class TextEditorExt implements theia.TextEditor { private disposed = false; constructor( private readonly proxy: TextEditorsMain, - private readonly id: string, + readonly id: string, document: DocumentDataExt, private _selections: Selection[], options: TextEditorConfiguration, @@ -499,7 +499,7 @@ export interface TextEditOperation { export interface EditData { documentVersionId: number; edits: TextEditOperation[]; - setEndOfLine: EndOfLine; + setEndOfLine: EndOfLine | undefined; undoStopBefore: boolean; undoStopAfter: boolean; } @@ -507,13 +507,12 @@ export interface EditData { export class TextEditorEdit { private readonly documentVersionId: number; private collectedEdits: TextEditOperation[]; - private eol: EndOfLine; + private eol: EndOfLine | undefined; private readonly undoStopBefore: boolean; private readonly undoStopAfter: boolean; constructor(private document: theia.TextDocument, options: { undoStopBefore: boolean; undoStopAfter: boolean }) { this.documentVersionId = document.version; this.collectedEdits = []; - this.eol = 0; this.undoStopBefore = options.undoStopBefore; this.undoStopAfter = options.undoStopAfter; } diff --git a/packages/plugin-ext/src/plugin/text-editors.ts b/packages/plugin-ext/src/plugin/text-editors.ts index 365a56acaad18..50ed6a3095335 100644 --- a/packages/plugin-ext/src/plugin/text-editors.ts +++ b/packages/plugin-ext/src/plugin/text-editors.ts @@ -21,11 +21,10 @@ import { Emitter, Event } from '@theia/core/lib/common/event'; import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; import { TextEditorExt } from './text-editor'; import * as Converters from './type-converters'; -import { TextEditorSelectionChangeKind } from './types-impl'; +import { TextEditorSelectionChangeKind, URI } from './types-impl'; import { IdGenerator } from '../common/id-generator'; export class TextEditorsExtImpl implements TextEditorsExt { - private readonly _onDidChangeTextEditorSelection = new Emitter(); private readonly _onDidChangeTextEditorOptions = new Emitter(); private readonly _onDidChangeTextEditorVisibleRanges = new Emitter(); @@ -124,6 +123,14 @@ export class TextEditorsExtImpl implements TextEditorsExt { return this.proxy.$tryApplyWorkspaceEdit(dto, metadata); } + save(uri: theia.Uri): PromiseLike { + return this.proxy.$save(uri).then(components => URI.revive(components)); + } + + saveAs(uri: theia.Uri): PromiseLike { + return this.proxy.$saveAs(uri).then(components => URI.revive(components)); + } + saveAll(includeUntitled?: boolean): PromiseLike { return this.proxy.$saveAll(includeUntitled); } diff --git a/packages/plugin-ext/src/plugin/tree/tree-views.ts b/packages/plugin-ext/src/plugin/tree/tree-views.ts index d7a130c2a3273..5dec3ca816fdd 100644 --- a/packages/plugin-ext/src/plugin/tree/tree-views.ts +++ b/packages/plugin-ext/src/plugin/tree/tree-views.ts @@ -446,7 +446,7 @@ class TreeViewExtImpl implements Disposable { } else if (ThemeIcon.is(iconPath)) { themeIcon = iconPath; } else { - iconUrl = PluginIconPath.toUrl(iconPath, this.plugin); + iconUrl = PluginIconPath.toUrl(iconPath, this.plugin); } let checkboxInfo; diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 512c81b359e75..c25ac586a7b78 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -16,7 +16,7 @@ import * as theia from '@theia/plugin'; import * as lstypes from '@theia/core/shared/vscode-languageserver-protocol'; -import { InlineValueEvaluatableExpression, InlineValueText, InlineValueVariableLookup, QuickPickItemKind, URI } from './types-impl'; +import { InlineValueEvaluatableExpression, InlineValueText, InlineValueVariableLookup, QuickPickItemKind, ThemeIcon, URI } from './types-impl'; import * as rpc from '../common/plugin-api-rpc'; import { DecorationOptions, EditorPosition, Plugin, Position, WorkspaceTextEditDto, WorkspaceFileEditDto, Selection, TaskDto, WorkspaceEditDto @@ -31,11 +31,11 @@ import { DisposableCollection, Mutable, isEmptyObject, isObject } from '@theia/c import * as notebooks from '@theia/notebook/lib/common'; import { CommandsConverter } from './command-registry'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; -import { CellData, CellExecutionUpdateType, CellOutput, CellOutputItem, CellRange, isTextStreamMime } from '@theia/notebook/lib/common'; -import { CellExecuteUpdate, CellExecutionComplete } from '@theia/notebook/lib/browser'; +import { CellRange, isTextStreamMime } from '@theia/notebook/lib/common'; import { MarkdownString as MarkdownStringDTO } from '@theia/core/lib/common/markdown-rendering'; -import { TestItemDTO, TestMessageDTO } from '../common/test-types'; +import { TestItemDTO, TestMessageDTO, TestMessageStackFrameDTO } from '../common/test-types'; +import { PluginIconPath } from './plugin-icon-path'; const SIDE_GROUP = -2; const ACTIVE_GROUP = -1; @@ -134,12 +134,21 @@ export function fromRange(range: theia.Range | undefined): model.Range | undefin endColumn: end.character + 1 }; } - -export function fromPosition(position: types.Position | theia.Position): Position { +export function fromPosition(position: types.Position | theia.Position): Position; +export function fromPosition(position: types.Position | theia.Position | undefined): Position | undefined; +export function fromPosition(position: types.Position | theia.Position | undefined): Position | undefined { + if (!position) { + return undefined; + } return { lineNumber: position.line + 1, column: position.character + 1 }; } -export function toPosition(position: Position): types.Position { +export function toPosition(position: Position): types.Position; +export function toPosition(position: Position | undefined): types.Position | undefined; +export function toPosition(position: Position | undefined): types.Position | undefined { + if (!position) { + return undefined; + } return new types.Position(position.lineNumber - 1, position.column - 1); } @@ -474,6 +483,18 @@ export function fromLocation(location: theia.Location | undefined): model.Locati }; } +export function fromLocationToLanguageServerLocation(location: theia.Location): lstypes.Location; +export function fromLocationToLanguageServerLocation(location: theia.Location | undefined): lstypes.Location | undefined; +export function fromLocationToLanguageServerLocation(location: theia.Location | undefined): lstypes.Location | undefined { + if (!location) { + return undefined; + } + return { + uri: location.uri.toString(), + range: location.range + }; +} + export function fromTextDocumentShowOptions(options: theia.TextDocumentShowOptions): model.TextDocumentShowOptions { if (options.selection) { return { @@ -731,6 +752,26 @@ export function toSymbolTag(kind: model.SymbolTag): types.SymbolTag { } } +/** + * Creates a merged symbol of type theia.SymbolInformation & theia.DocumentSymbol. + * Is only used as the result type of the `vscode.executeDocumentSymbolProvider` command. + */ +export function toMergedSymbol(uri: UriComponents, symbol: model.DocumentSymbol): theia.SymbolInformation & theia.DocumentSymbol { + const uriValue = URI.revive(uri); + const location = new types.Location(uriValue, toRange(symbol.range)); + return { + name: symbol.name, + containerName: symbol.containerName ?? '', + kind: SymbolKind.toSymbolKind(symbol.kind), + tags: [], + location, + detail: symbol.detail, + range: location.range, + selectionRange: toRange(symbol.selectionRange), + children: symbol.children?.map(child => toMergedSymbol(uri, child)) ?? [] + }; +} + export function isModelLocation(arg: unknown): arg is model.Location { return isObject(arg) && isModelRange(arg.range) && @@ -748,9 +789,10 @@ export function isModelRange(arg: unknown): arg is model.Range { export function isUriComponents(arg: unknown): arg is UriComponents { return isObject(arg) && typeof arg.scheme === 'string' && - typeof arg.path === 'string' && - typeof arg.query === 'string' && - typeof arg.fragment === 'string'; + (arg['$mid'] === 1 || ( + typeof arg.path === 'string' && + typeof arg.query === 'string' && + typeof arg.fragment === 'string')); } export function isModelCallHierarchyItem(arg: unknown): arg is model.CallHierarchyItem { @@ -1213,23 +1255,57 @@ export function fromColorPresentation(colorPresentation: theia.ColorPresentation }; } -export function convertToTransferQuickPickItems(items: rpc.Item[]): rpc.TransferQuickPickItems[] { - return items.map((item, index) => { +export function convertIconPath(iconPath: types.URI | { light: types.URI; dark: types.URI } | theia.ThemeIcon | undefined): + UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { + if (!iconPath) { + return undefined; + } + if (iconPath instanceof types.URI) { + return iconPath.toJSON(); + } else if ('dark' in iconPath) { + return { + dark: iconPath.dark.toJSON(), + light: iconPath.light?.toJSON() + }; + } else if (ThemeIcon.is(iconPath)) { + return { + id: iconPath.id, + color: iconPath.color ? { id: iconPath.color.id } : undefined + }; + } else { + return undefined; + } +} + +export function convertQuickInputButton(plugin: Plugin, button: theia.QuickInputButton, index: number): rpc.TransferQuickInputButton { + const iconPath = convertIconPath(button.iconPath); + if (!iconPath) { + throw new Error(`Could not convert icon path: '${button.iconPath}'`); + } + return { + handle: index, + iconUrl: PluginIconPath.toUrl(iconPath, plugin) ?? ThemeIcon.get(iconPath), + tooltip: button.tooltip + }; +} + +export function convertToTransferQuickPickItems(plugin: Plugin, items: (theia.QuickPickItem | string)[]): rpc.TransferQuickPickItem[] { + return items.map((item, index) => { if (typeof item === 'string') { - return { type: 'item', label: item, handle: index }; + return { kind: 'item', label: item, handle: index }; } else if (item.kind === QuickPickItemKind.Separator) { - return { type: 'separator', label: item.label, handle: index }; + return { kind: 'separator', label: item.label, handle: index }; } else { const { label, description, iconPath, detail, picked, alwaysShow, buttons } = item; return { - type: 'item', + kind: 'item', label, description, - iconPath, + iconUrl: PluginIconPath.toUrl(iconPath, plugin) ?? ThemeIcon.get(iconPath), detail, picked, alwaysShow, - buttons, + buttons: buttons ? buttons.map((button, i) => convertQuickInputButton(plugin, button, i)) : undefined, handle: index, }; } @@ -1404,7 +1480,7 @@ export namespace DataTransferItem { return { name: file.name, uri: URI.revive(file.uri), - data: () => resolveFileData(item.id), + data: () => resolveFileData(file.id), }; } }(''); @@ -1487,8 +1563,8 @@ export namespace NotebookCellData { cellKind: NotebookCellKind.from(data.kind), language: data.languageId, source: data.value, - // metadata: data.metadata, - // internalMetadata: NotebookCellExecutionSummary.from(data.executionSummary ?? {}), + metadata: data.metadata, + internalMetadata: NotebookCellExecutionSummary.from(data.executionSummary ?? {}), outputs: data.outputs ? data.outputs.map(NotebookCellOutputConverter.from) : [] }; } @@ -1636,140 +1712,32 @@ export namespace NotebookKernelSourceAction { } } -export namespace NotebookDto { - - export function toNotebookOutputItemDto(item: CellOutputItem): rpc.NotebookOutputItemDto { - return { - mime: item.mime, - valueBytes: item.data - }; - } - - export function toNotebookOutputDto(output: CellOutput): rpc.NotebookOutputDto { - return { - outputId: output.outputId, - metadata: output.metadata, - items: output.outputs.map(toNotebookOutputItemDto) - }; - } - - export function toNotebookCellDataDto(cell: CellData): rpc.NotebookCellDataDto { - return { - cellKind: cell.cellKind, - language: cell.language, - source: cell.source, - internalMetadata: cell.internalMetadata, - metadata: cell.metadata, - outputs: cell.outputs.map(toNotebookOutputDto) - }; - } - - // export function toNotebookDataDto(data: NotebookData): rpc.NotebookDataDto { - // return { - // metadata: data.metadata, - // cells: data.cells.map(toNotebookCellDataDto) - // }; - // } - - export function fromNotebookOutputItemDto(item: rpc.NotebookOutputItemDto): CellOutputItem { - return { - mime: item.mime, - data: item.valueBytes - }; - } - - export function fromNotebookOutputDto(output: rpc.NotebookOutputDto): CellOutput { - return { - outputId: output.outputId, - metadata: output.metadata, - outputs: output.items.map(fromNotebookOutputItemDto) - }; - } - - export function fromNotebookCellDataDto(cell: rpc.NotebookCellDataDto): CellData { - return { - cellKind: cell.cellKind, - language: cell.language, - source: cell.source, - outputs: cell.outputs.map(fromNotebookOutputDto), - metadata: cell.metadata, - internalMetadata: cell.internalMetadata - }; - } - - // export function fromNotebookDataDto(data: rpc.NotebookDataDto): NotebookData { - // return { - // metadata: data.metadata, - // cells: data.cells.map(fromNotebookCellDataDto) - // }; - // } - - // export function toNotebookCellDto(cell: Cell): rpc.NotebookCellDto { - // return { - // handle: cell.handle, - // uri: cell.uri, - // source: cell.textBuffer.getLinesContent(), - // eol: cell.textBuffer.getEOL(), - // language: cell.language, - // cellKind: cell.cellKind, - // outputs: cell.outputs.map(toNotebookOutputDto), - // metadata: cell.metadata, - // internalMetadata: cell.internalMetadata, - // }; - // } - - export function fromCellExecuteUpdateDto(data: rpc.CellExecuteUpdateDto): CellExecuteUpdate { - if (data.editType === CellExecutionUpdateType.Output) { - return { - editType: data.editType, - cellHandle: data.cellHandle, - append: data.append, - outputs: data.outputs.map(fromNotebookOutputDto) - }; - } else if (data.editType === CellExecutionUpdateType.OutputItems) { - return { - editType: data.editType, - append: data.append, - outputId: data.outputId, - items: data.items.map(fromNotebookOutputItemDto) - }; - } else { - return data; - } - } - - export function fromCellExecuteCompleteDto(data: rpc.CellExecutionCompleteDto): CellExecutionComplete { - return data; - } - - // export function fromCellEditOperationDto(edit: rpc.CellEditOperationDto): CellEditOperation { - // if (edit.editType === CellEditType.Replace) { - // return { - // editType: edit.editType, - // index: edit.index, - // count: edit.count, - // cells: edit.cells.map(fromNotebookCellDataDto) - // }; - // } else { - // return edit; - // } - // } -} - export namespace TestMessage { export function from(message: theia.TestMessage | readonly theia.TestMessage[]): TestMessageDTO[] { if (isReadonlyArray(message)) { return message.map(msg => TestMessage.from(msg)[0]); } return [{ - location: fromLocation(message.location), + location: fromLocationToLanguageServerLocation(message.location), message: fromMarkdown(message.message)!, expected: message.expectedOutput, - actual: message.actualOutput + actual: message.actualOutput, + contextValue: message.contextValue, + stackTrace: message.stackTrace && message.stackTrace.map(frame => TestMessageStackFrame.from(frame)) }]; } } +export namespace TestMessageStackFrame { + export function from(stackTrace: theia.TestMessageStackFrame): TestMessageStackFrameDTO { + return { + label: stackTrace.label, + position: stackTrace.position, + uri: stackTrace?.uri?.toString() + }; + } +} + export namespace TestItem { export function from(test: theia.TestItem): TestItemDTO { return TestItem.fromPartial(test); diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 5e3f05a21e665..d7a0734259657 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -79,7 +79,7 @@ export class URI extends CodeURI implements theia.Uri { */ static override revive(data: UriComponents | CodeURI): URI; static override revive(data: UriComponents | CodeURI | null): URI | null; - static override revive(data: UriComponents | CodeURI | undefined): URI | undefined + static override revive(data: UriComponents | CodeURI | undefined): URI | undefined; static override revive(data: UriComponents | CodeURI | undefined | null): URI | undefined | null { const uri = CodeURI.revive(data); return uri ? new URI(uri) : undefined; @@ -153,7 +153,8 @@ export enum StatusBarAlignment { export enum TextEditorLineNumbersStyle { Off = 0, On = 1, - Relative = 2 + Relative = 2, + Interval = 3 } /** @@ -743,6 +744,9 @@ export namespace ThemeIcon { export function is(item: unknown): item is ThemeIcon { return isObject(item) && 'id' in item; } + export function get(item: unknown): ThemeIcon | undefined { + return is(item) ? item : undefined; + } } export enum TextEditorRevealType { @@ -881,7 +885,7 @@ export class TextEdit { protected _range: Range; protected _newText: string; - protected _newEol: EndOfLine; + protected _newEol: EndOfLine | undefined; get range(): Range { return this._range; @@ -905,7 +909,7 @@ export class TextEdit { this._newText = value; } - get newEol(): EndOfLine { + get newEol(): EndOfLine | undefined { return this._newEol; } @@ -1593,6 +1597,30 @@ export class DocumentHighlight { } } +@es5ClassCompat +export class MultiDocumentHighlight { + + /** + * The URI of the document containing the highlights. + */ + uri: URI; + + /** + * The highlights for the document. + */ + highlights: DocumentHighlight[]; + + /** + * Creates a new instance of MultiDocumentHighlight. + * @param uri The URI of the document containing the highlights. + * @param highlights The highlights for the document. + */ + constructor(uri: URI, highlights: DocumentHighlight[]) { + this.uri = uri; + this.highlights = highlights; + } +} + export type Definition = Location | Location[]; @es5ClassCompat @@ -1617,16 +1645,35 @@ export class DocumentLink { } @es5ClassCompat -export class DocumentDropEdit { +export class DocumentDropOrPasteEditKind { + static readonly Empty: DocumentDropOrPasteEditKind = new DocumentDropOrPasteEditKind(''); - id?: string; + private static sep = '.'; - priority?: number; + constructor( + public readonly value: string + ) { } - label?: string; + public append(...parts: string[]): DocumentDropOrPasteEditKind { + return new DocumentDropOrPasteEditKind((this.value ? [this.value, ...parts] : parts).join(DocumentDropOrPasteEditKind.sep)); + } - insertText: string | SnippetString; + public intersects(other: DocumentDropOrPasteEditKind): boolean { + return this.contains(other) || other.contains(this); + } + public contains(other: DocumentDropOrPasteEditKind): boolean { + return this.value === other.value || other.value.startsWith(this.value + DocumentDropOrPasteEditKind.sep); + } +} + +@es5ClassCompat +export class DocumentDropEdit { + title?: string; + kind: DocumentDropOrPasteEditKind; + handledMimeType?: string; + yieldTo?: ReadonlyArray; + insertText: string | SnippetString; additionalEdit?: WorkspaceEdit; constructor(insertText: string | SnippetString) { @@ -1845,13 +1892,13 @@ export class WorkspaceEdit implements theia.WorkspaceEdit { } set(uri: URI, edits: ReadonlyArray): void; - set(uri: URI, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, theia.WorkspaceEditEntryMetadata]>): void; + set(uri: URI, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, theia.WorkspaceEditEntryMetadata | undefined]>): void; set(uri: URI, edits: ReadonlyArray): void; - set(uri: URI, edits: ReadonlyArray<[NotebookEdit, theia.WorkspaceEditEntryMetadata]>): void; + set(uri: URI, edits: ReadonlyArray<[NotebookEdit, theia.WorkspaceEditEntryMetadata | undefined]>): void; set(uri: URI, edits: ReadonlyArray): void { + | NotebookEdit | [NotebookEdit, theia.WorkspaceEditEntryMetadata | undefined] + | [TextEdit | SnippetTextEdit, theia.WorkspaceEditEntryMetadata | undefined]>): void { if (!edits) { // remove all text edits for `uri` for (let i = 0; i < this._edits.length; i++) { @@ -2039,8 +2086,8 @@ export class TreeItem { readonly accessibilityInformation?: AccessibilityInformation }; - constructor(label: string | theia.TreeItemLabel, collapsibleState?: theia.TreeItemCollapsibleState) - constructor(resourceUri: URI, collapsibleState?: theia.TreeItemCollapsibleState) + constructor(label: string | theia.TreeItemLabel, collapsibleState?: theia.TreeItemCollapsibleState); + constructor(resourceUri: URI, collapsibleState?: theia.TreeItemCollapsibleState); constructor(arg1: string | theia.TreeItemLabel | URI, public collapsibleState: theia.TreeItemCollapsibleState = TreeItemCollapsibleState.None) { if (arg1 instanceof URI) { this.resourceUri = arg1; @@ -2984,6 +3031,14 @@ export class FunctionBreakpoint extends Breakpoint { } } +export class DebugThread implements theia.DebugThread { + constructor(readonly session: theia.DebugSession, readonly threadId: number) { } +} + +export class DebugStackFrame implements theia.DebugStackFrame { + constructor(readonly session: theia.DebugSession, readonly threadId: number, readonly frameId: number) { } +} + @es5ClassCompat export class Color { readonly red: number; @@ -3285,6 +3340,7 @@ export class TestRunRequest implements theia.TestRunRequest { public readonly exclude: theia.TestItem[] | undefined = undefined, public readonly profile: theia.TestRunProfile | undefined = undefined, public readonly continuous: boolean | undefined = undefined, + public readonly preserveFocus: boolean = true ) { } } @@ -3293,6 +3349,8 @@ export class TestMessage implements theia.TestMessage { public expectedOutput?: string; public actualOutput?: string; public location?: theia.Location; + public contextValue?: string; + public stackTrace?: theia.TestMessageStackFrame[] | undefined; public static diff(message: string | theia.MarkdownString, expected: string, actual: string): theia.TestMessage { const msg = new TestMessage(message); @@ -3304,6 +3362,80 @@ export class TestMessage implements theia.TestMessage { constructor(public message: string | theia.MarkdownString) { } } +@es5ClassCompat +export class TestCoverageCount { + constructor(public covered: number, public total: number) { } +} + +export class TestMessageStackFrame implements theia.TestMessageStackFrame { + constructor( + public label: string, + public uri?: theia.Uri, + public position?: Position + ) { } +} + +@es5ClassCompat +export class FileCoverage { + + detailedCoverage?: theia.FileCoverageDetail[]; + + static fromDetails(uri: theia.Uri, details: theia.FileCoverageDetail[]): FileCoverage { + const statements = new TestCoverageCount(0, 0); + const branches = new TestCoverageCount(0, 0); + const decl = new TestCoverageCount(0, 0); + + for (const detail of details) { + if (detail instanceof StatementCoverage) { + statements.total += 1; + statements.covered += detail.executed ? 1 : 0; + + for (const branch of detail.branches) { + branches.total += 1; + branches.covered += branch.executed ? 1 : 0; + } + } else { + decl.total += 1; + decl.covered += detail.executed ? 1 : 0; + } + } + + const coverage = new FileCoverage( + uri, + statements, + branches.total > 0 ? branches : undefined, + decl.total > 0 ? decl : undefined, + ); + + coverage.detailedCoverage = details; + + return coverage; + } + + constructor( + public uri: theia.Uri, + public statementCoverage: TestCoverageCount, + public branchCoverage?: TestCoverageCount, + public declarationCoverage?: TestCoverageCount, + ) { } +} + +@es5ClassCompat +export class StatementCoverage implements theia.StatementCoverage { + constructor(public executed: number | boolean, public location: Position | Range, public branches: BranchCoverage[] = []) { } +} + +export class BranchCoverage implements theia.BranchCoverage { + constructor(public executed: number | boolean, public location?: Position | Range, public label?: string) { } +} + +@es5ClassCompat +export class DeclarationCoverage implements theia.DeclarationCoverage { + constructor(public name: string, public executed: number | boolean, public location: Position | Range) { } +} + +export type FileCoverageDetail = StatementCoverage | DeclarationCoverage; + @es5ClassCompat export class TimelineItem { timestamp: number; @@ -3648,19 +3780,57 @@ export class InteractiveWindowInput { // #endregion // #region DocumentPaste +export class DocumentPasteEditKind { + static Empty: DocumentPasteEditKind; + + constructor(public readonly value: string) { } + + /** @stubbed */ + append(...parts: string[]): CodeActionKind { + return CodeActionKind.Empty; + }; + + /** @stubbed */ + intersects(other: CodeActionKind): boolean { + return false; + } + + /** @stubbed */ + contains(other: CodeActionKind): boolean { + return false; + } +} +DocumentPasteEditKind.Empty = new DocumentPasteEditKind(''); + @es5ClassCompat export class DocumentPasteEdit { - constructor(insertText: string | SnippetString, id: string, label: string) { + constructor(insertText: string | SnippetString, title: string, kind: DocumentDropOrPasteEditKind) { this.insertText = insertText; - this.id = id; - this.label = label; + this.title = title; + this.kind = kind; } + title: string; + kind: DocumentDropOrPasteEditKind; insertText: string | SnippetString; additionalEdit?: WorkspaceEdit; - id: string; - label: string; - priority?: number; + yieldTo?: ReadonlyArray; +} + +/** + * The reason why paste edits were requested. + */ +export enum DocumentPasteTriggerKind { + /** + * Pasting was requested as part of a normal paste operation. + */ + Automatic = 0, + + /** + * Pasting was requested by the user with the `paste as` command. + */ + PasteAs = 1, } + // #endregion // #region DocumentPaste @@ -3672,15 +3842,19 @@ export enum EditSessionIdentityMatch { // #endregion // #region terminalQuickFixProvider -export class TerminalQuickFixExecuteTerminalCommand { +export class TerminalQuickFixTerminalCommand { /** * The terminal command to run */ terminalCommand: string; + /** + * Whether the command should be executed or just inserted (default) + */ + shouldExecute?: boolean; /** * @stubbed */ - constructor(terminalCommand: string) { } + constructor(terminalCommand: string, shouldExecute?: boolean) { } } export class TerminalQuickFixOpener { /** @@ -3693,3 +3867,154 @@ export class TerminalQuickFixOpener { constructor(uri: theia.Uri) { } } +// #region Chat +export class ChatRequestTurn { + readonly prompt: string; + readonly participant: string; + readonly command?: string; + readonly references: theia.ChatPromptReference[]; + private constructor(prompt: string, command: string | undefined, references: theia.ChatPromptReference[], participant: string) { + this.prompt = prompt; + this.command = command; + this.participant = participant; + this.references = references; + }; +} + +export class ChatResponseTurn { + readonly command?: string; + + private constructor(readonly response: ReadonlyArray, readonly result: theia.ChatResult, readonly participant: string) { } +} + +export class ChatResponseAnchorPart { + value: URI | Location; + title?: string; + + constructor(value: URI | Location, title?: string) { } +} + +export class ChatResponseProgressPart { + value: string; + + constructor(value: string) { } +} + +export class ChatResponseReferencePart { + value: URI | Location; + iconPath?: URI | ThemeIcon | { light: URI; dark: URI; }; + + constructor(value: URI | theia.Location, iconPath?: URI | ThemeIcon | { + light: URI; + dark: URI; + }) { } +} +export class ChatResponseCommandButtonPart { + value: theia.Command; + + constructor(value: theia.Command) { } +} + +export class ChatResponseMarkdownPart { + value: theia.MarkdownString; + + constructor(value: string | theia.MarkdownString) { + } +} + +export class ChatResponseFileTreePart { + value: theia.ChatResponseFileTree[]; + baseUri: URI; + + constructor(value: theia.ChatResponseFileTree[], baseUri: URI) { } +} + +export type ChatResponsePart = ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart + | ChatResponseProgressPart | ChatResponseReferencePart | ChatResponseCommandButtonPart; + +export enum ChatResultFeedbackKind { + Unhelpful = 0, + Helpful = 1, +} + +export enum LanguageModelChatMessageRole { + User = 1, + Assistant = 2 +} + +export class LanguageModelChatMessage { + + static User(content: string, name?: string): LanguageModelChatMessage { + return new LanguageModelChatMessage(LanguageModelChatMessageRole.User, content, name); + } + + static Assistant(content: string, name?: string): LanguageModelChatMessage { + return new LanguageModelChatMessage(LanguageModelChatMessageRole.Assistant, content, name); + } + + constructor(public role: LanguageModelChatMessageRole, public content: string, public name?: string) { } +} + +export class LanguageModelError extends Error { + + static NoPermissions(message?: string): LanguageModelError { + return new LanguageModelError(message, LanguageModelError.NoPermissions.name); + } + + static Blocked(message?: string): LanguageModelError { + return new LanguageModelError(message, LanguageModelError.Blocked.name); + } + + static NotFound(message?: string): LanguageModelError { + return new LanguageModelError(message, LanguageModelError.NotFound.name); + } + + readonly code: string; + + constructor(message?: string, code?: string) { + super(message); + this.name = 'LanguageModelError'; + this.code = code ?? ''; + } +} +// #endregion + +// #region Port Attributes + +export enum PortAutoForwardAction { + Notify = 1, + OpenBrowser = 2, + OpenPreview = 3, + Silent = 4, + Ignore = 5 +} + +export class PortAttributes { + constructor(public autoForwardAction: PortAutoForwardAction) { + } +} + +// #endregion + +// #region Debug Visualization + +export class DebugVisualization { + iconPath?: URI | { light: URI; dark: URI } | ThemeIcon; + visualization?: theia.Command | { treeId: string }; + + constructor(public name: string) { + } +} + +// #endregion + +// #region Terminal Shell Integration + +export enum TerminalShellExecutionCommandLineConfidence { + Low = 0, + Medium = 1, + High = 2 +} + +// #endregion diff --git a/packages/plugin-ext/src/plugin/uri-ext.ts b/packages/plugin-ext/src/plugin/uri-ext.ts new file mode 100644 index 0000000000000..1a81cc5590c69 --- /dev/null +++ b/packages/plugin-ext/src/plugin/uri-ext.ts @@ -0,0 +1,60 @@ +// ***************************************************************************** +// Copyright (C) 2024 STMicroelectronics. +// +// 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 theia from '@theia/plugin'; +import { + UriExt, + PLUGIN_RPC_CONTEXT, PluginInfo, UriMain +} from '../common/plugin-api-rpc'; +import { RPCProtocol } from '../common/rpc-protocol'; +import { Disposable, URI } from './types-impl'; +import { UriComponents } from '../common/uri-components'; + +export class UriExtImpl implements UriExt { + + private handlers = new Map(); + + private readonly proxy: UriMain; + + constructor(readonly rpc: RPCProtocol) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.URI_MAIN); + console.log(this.proxy); + } + + registerUriHandler(handler: theia.UriHandler, plugin: PluginInfo): theia.Disposable { + const pluginId = plugin.id; + if (this.handlers.has(pluginId)) { + throw new Error(`URI handler already registered for plugin ${pluginId}`); + } + + this.handlers.set(pluginId, handler); + this.proxy.$registerUriHandler(pluginId, plugin.displayName || plugin.name); + + return new Disposable(() => { + this.proxy.$unregisterUriHandler(pluginId); + this.handlers.delete(pluginId); + }); + } + + $handleExternalUri(uri: UriComponents): Promise { + const handler = this.handlers.get(uri.authority); + if (!handler) { + return Promise.resolve(); + } + handler.handleUri(URI.revive(uri)); + return Promise.resolve(); + } +} diff --git a/packages/plugin-ext/src/plugin/webview-views.ts b/packages/plugin-ext/src/plugin/webview-views.ts index 04d66415b79e3..16d659c691c29 100644 --- a/packages/plugin-ext/src/plugin/webview-views.ts +++ b/packages/plugin-ext/src/plugin/webview-views.ts @@ -27,6 +27,7 @@ import { WebviewImpl, WebviewsExtImpl } from './webviews'; import { WebviewViewProvider } from '@theia/plugin'; import { Emitter, Event } from '@theia/core/lib/common/event'; import * as theia from '@theia/plugin'; +import { hashValue } from '@theia/core/lib/common/uuid'; export class WebviewViewsExtImpl implements WebviewViewsExt { @@ -82,7 +83,7 @@ export class WebviewViewsExtImpl implements WebviewViewsExt { const { provider, plugin } = entry; - const webviewNoPanel = this.webviewsExt.createNewWebview({}, plugin, handle); + const webviewNoPanel = this.webviewsExt.createNewWebview({}, plugin, handle, hashValue(viewType)); const revivedView = new WebviewViewExtImpl(handle, this.proxy, viewType, title, webviewNoPanel, true); this.webviewViews.set(handle, revivedView); await provider.resolveWebviewView(revivedView, { state }, cancellation); diff --git a/packages/plugin-ext/src/plugin/webviews.ts b/packages/plugin-ext/src/plugin/webviews.ts index c107ced1af6be..d910fe8beda02 100644 --- a/packages/plugin-ext/src/plugin/webviews.ts +++ b/packages/plugin-ext/src/plugin/webviews.ts @@ -14,7 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { v4 } from 'uuid'; +import { generateUuid, hashValue } from '@theia/core/lib/common/uuid'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { Plugin, WebviewsExt, WebviewPanelViewState, WebviewsMain, PLUGIN_RPC_CONTEXT, WebviewInitData, /* WebviewsMain, PLUGIN_RPC_CONTEXT */ } from '../common/plugin-api-rpc'; import * as theia from '@theia/plugin'; import { RPCProtocol } from '../common/rpc-protocol'; @@ -23,9 +24,17 @@ import { fromViewColumn, toViewColumn, toWebviewPanelShowOptions } from './type- import { Disposable, WebviewPanelTargetArea, URI } from './types-impl'; import { WorkspaceExtImpl } from './workspace'; import { PluginIconPath } from './plugin-icon-path'; +import { PluginModel, PluginPackage } from '../common'; +@injectable() export class WebviewsExtImpl implements WebviewsExt { - private readonly proxy: WebviewsMain; + @inject(RPCProtocol) + protected readonly rpc: RPCProtocol; + + @inject(WorkspaceExtImpl) + protected readonly workspace: WorkspaceExtImpl; + + private proxy: WebviewsMain; private readonly webviewPanels = new Map(); private readonly webviews = new Map(); private readonly serializers = new Map(); readonly onDidDispose: Event = this.onDidDisposeEmitter.event; - constructor( - rpc: RPCProtocol, - private readonly workspace: WorkspaceExtImpl, - ) { - this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.WEBVIEWS_MAIN); + @postConstruct() + initialize(): void { + this.proxy = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.WEBVIEWS_MAIN); } init(initData: WebviewInitData): void { @@ -96,7 +103,7 @@ export class WebviewsExtImpl implements WebviewsExt { } const { serializer, plugin } = entry; - const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin); + const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin, hashValue(viewType)); const revivedPanel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, toViewColumn(viewState.position)!, options, webview); revivedPanel.setActive(viewState.active); revivedPanel.setVisible(viewState.visible); @@ -111,7 +118,7 @@ export class WebviewsExtImpl implements WebviewsExt { options: theia.WebviewPanelOptions & theia.WebviewOptions, plugin: Plugin ): theia.WebviewPanel { - const viewId = v4(); + const viewId = generateUuid(); const webviewShowOptions = toWebviewPanelShowOptions(showOptions); const webviewOptions = WebviewImpl.toWebviewOptions(options, this.workspace, plugin); this.proxy.$createWebviewPanel(viewId, viewType, title, webviewShowOptions, webviewOptions); @@ -119,19 +126,33 @@ export class WebviewsExtImpl implements WebviewsExt { return panel; } + /** + * Creates a new webview panel. + * + * @param viewType Identifies the type of the webview panel. + * @param title Title of the panel. + * @param showOptions Where webview panel will be reside. + * @param options Settings for the new panel. + * @param plugin The plugin contributing the webview. + * @param viewId The identifier of the webview instance. + * @param originBasedOnType true if a stable origin based on the viewType shall be used, false if the viewId should be used. + * @returns The new webview panel. + */ createWebviewPanel( viewType: string, title: string, showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions, options: theia.WebviewPanelOptions & theia.WebviewOptions, plugin: Plugin, - viewId: string + viewId: string, + originBasedOnType = true ): WebviewPanelImpl { if (!this.initData) { throw new Error('Webviews are not initialized'); } const webviewShowOptions = toWebviewPanelShowOptions(showOptions); - const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin); + const origin = originBasedOnType ? hashValue(viewType) : undefined; + const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin, origin); const panel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, webviewShowOptions, options, webview); this.webviewPanels.set(viewId, panel); return panel; @@ -140,12 +161,13 @@ export class WebviewsExtImpl implements WebviewsExt { createNewWebview( options: theia.WebviewPanelOptions & theia.WebviewOptions, plugin: Plugin, - viewId: string + viewId: string, + origin?: string ): WebviewImpl { if (!this.initData) { throw new Error('Webviews are not initialized'); } - const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin); + const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin, origin); this.webviews.set(viewId, webview); return webview; } @@ -175,6 +197,14 @@ export class WebviewsExtImpl implements WebviewsExt { return undefined; } + toGeneralWebviewResource(extension: PluginModel, resource: theia.Uri): theia.Uri { + const extensionUri = URI.parse(extension.packageUri); + const relativeResourcePath = resource.path.replace(extensionUri.path, ''); + const basePath = PluginPackage.toPluginUrl(extension, '') + relativeResourcePath; + + return URI.parse(this.initData!.webviewResourceRoot.replace('{{uuid}}', 'webviewUUID')).with({ path: basePath }); + } + public deleteWebview(handle: string): void { this.webviews.delete(handle); } @@ -182,6 +212,10 @@ export class WebviewsExtImpl implements WebviewsExt { public getWebview(handle: string): WebviewImpl | undefined { return this.webviews.get(handle); } + + public getResourceRoot(): string | undefined { + return this.initData?.webviewResourceRoot; + } } export class WebviewImpl implements theia.Webview { @@ -201,7 +235,8 @@ export class WebviewImpl implements theia.Webview { options: theia.WebviewOptions, private readonly initData: WebviewInitData, private readonly workspace: WorkspaceExtImpl, - readonly plugin: Plugin + readonly plugin: Plugin, + private readonly origin?: string ) { this._options = options; } @@ -219,12 +254,12 @@ export class WebviewImpl implements theia.Webview { .replace('{{scheme}}', resource.scheme) .replace('{{authority}}', resource.authority) .replace('{{path}}', resource.path.replace(/^\//, '')) - .replace('{{uuid}}', this.viewId); - return URI.parse(uri); + .replace('{{uuid}}', this.origin ?? this.viewId); + return URI.parse(uri).with({ query: resource.query }); } get cspSource(): string { - return this.initData.webviewCspSource.replace('{{uuid}}', this.viewId); + return this.initData.webviewCspSource.replace('{{uuid}}', this.origin ?? this.viewId); } get html(): string { diff --git a/packages/plugin-ext/src/plugin/window-state.ts b/packages/plugin-ext/src/plugin/window-state.ts index 160356282c8e9..9492973d6250e 100644 --- a/packages/plugin-ext/src/plugin/window-state.ts +++ b/packages/plugin-ext/src/plugin/window-state.ts @@ -19,7 +19,6 @@ import { WindowState } from '@theia/plugin'; import { WindowStateExt, WindowMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { RPCProtocol } from '../common/rpc-protocol'; -import { Schemes } from '../common/uri-components'; export class WindowStateExtImpl implements WindowStateExt { @@ -32,24 +31,41 @@ export class WindowStateExtImpl implements WindowStateExt { constructor(rpc: RPCProtocol) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.WINDOW_MAIN); - this.windowStateCached = { focused: true }; // supposed tab is active on start + this.windowStateCached = { focused: true, active: true }; // supposed tab is active on start } getWindowState(): WindowState { return this.windowStateCached; } - $onWindowStateChanged(focused: boolean): void { - const state = { focused: focused }; - if (state === this.windowStateCached) { + $onDidChangeWindowFocus(focused: boolean): void { + this.onDidChangeWindowProperty('focused', focused); + } + + $onDidChangeWindowActive(active: boolean): void { + this.onDidChangeWindowProperty('active', active); + } + + onDidChangeWindowProperty(property: keyof WindowState, value: boolean): void { + if (value === this.windowStateCached[property]) { return; } - this.windowStateCached = state; - this.windowStateChangedEmitter.fire(state); + this.windowStateCached = { ...this.windowStateCached, [property]: value }; + this.windowStateChangedEmitter.fire(this.windowStateCached); } - openUri(uri: URI): Promise { + async openUri(uriOrString: URI | string): Promise { + let uri: URI; + if (typeof uriOrString === 'string') { + uri = URI.parse(uriOrString); + } else { + uri = uriOrString; + } + if (!uri.scheme.trim().length) { + throw new Error('Invalid scheme - cannot be empty'); + } + return this.proxy.$openUri(uri); } @@ -57,9 +73,6 @@ export class WindowStateExtImpl implements WindowStateExt { if (!target.scheme.trim().length) { throw new Error('Invalid scheme - cannot be empty'); } - if (Schemes.http !== target.scheme && Schemes.https !== target.scheme) { - throw new Error(`Invalid scheme '${target.scheme}'`); - } const uri = await this.proxy.$asExternalUri(target); return URI.revive(uri); diff --git a/packages/plugin-ext/src/plugin/workspace.ts b/packages/plugin-ext/src/plugin/workspace.ts index ea30c844dc6ea..9f8a1d812193d 100644 --- a/packages/plugin-ext/src/plugin/workspace.ts +++ b/packages/plugin-ext/src/plugin/workspace.ts @@ -21,6 +21,7 @@ import * as paths from 'path'; import * as theia from '@theia/plugin'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { @@ -44,8 +45,18 @@ import * as Converter from './type-converters'; import { FileStat } from '@theia/filesystem/lib/common/files'; import { isUndefinedOrNull, isUndefined } from '../common/types'; +@injectable() export class WorkspaceExtImpl implements WorkspaceExt { + @inject(RPCProtocol) + protected readonly rpc: RPCProtocol; + + @inject(EditorsAndDocumentsExtImpl) + protected editorsAndDocuments: EditorsAndDocumentsExtImpl; + + @inject(MessageRegistryExt) + protected messageService: MessageRegistryExt; + private proxy: WorkspaceMain; private workspaceFoldersChangedEmitter = new Emitter(); @@ -63,10 +74,9 @@ export class WorkspaceExtImpl implements WorkspaceExt { private canonicalUriProviders = new Map(); - constructor(rpc: RPCProtocol, - private editorsAndDocuments: EditorsAndDocumentsExtImpl, - private messageService: MessageRegistryExt) { - this.proxy = rpc.getProxy(Ext.WORKSPACE_MAIN); + @postConstruct() + initialize(): void { + this.proxy = this.rpc.getProxy(Ext.WORKSPACE_MAIN); } get rootPath(): string | undefined { diff --git a/packages/plugin-metrics/package.json b/packages/plugin-metrics/package.json index fb03ce94327fb..4852c50738780 100644 --- a/packages/plugin-metrics/package.json +++ b/packages/plugin-metrics/package.json @@ -1,13 +1,14 @@ { "name": "@theia/plugin-metrics", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Plugin Metrics", "dependencies": { - "@theia/core": "1.44.0", - "@theia/metrics": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/plugin": "1.44.0", - "@theia/plugin-ext": "1.44.0" + "@theia/core": "1.54.0", + "@theia/metrics": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", + "@theia/plugin": "1.54.0", + "@theia/plugin-ext": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -43,7 +44,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 1413060ae147a..b9c572034c9e6 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@theia/plugin", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Plugin API", "types": "./src/theia.d.ts", "publishConfig": { @@ -27,7 +27,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/plugin/src/theia-extra.d.ts b/packages/plugin/src/theia-extra.d.ts index f1826c780dd75..54bbd49413dfd 100644 --- a/packages/plugin/src/theia-extra.d.ts +++ b/packages/plugin/src/theia-extra.d.ts @@ -291,6 +291,13 @@ export module '@theia/plugin' { * see - workspace.fs for how to read and write files and folders from an uri. */ readonly logUri: Uri; + + /** + * An object that keeps information about how this extension can use language models. + * + * @see {@link LanguageModelChat.sendRequest} + */ + readonly languageModelAccessInformation: LanguageModelAccessInformation; } export namespace commands { @@ -363,6 +370,26 @@ export module '@theia/plugin' { color?: ThemeColor; } + export interface TerminalObserver { + + /** + * A regex to match against the latest terminal output. + */ + readonly outputMatcherRegex: string; + /** + * The maximum number of lines to match the regex against. Maximum is 40 lines. + */ + readonly nrOfLinesToMatch: number; + /** + * Invoked when the regex matched against the terminal contents. + * @param groups The matched groups + */ + matchOccurred(groups: string[]): void; + } + + export namespace window { + export function registerTerminalObserver(observer: TerminalObserver): Disposable; + } } /** diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 31847322fd15f..c98c3ccef5691 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -3,7 +3,7 @@ // // 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. +// http://www.eclipse.org/legal/epl-2.0.g // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse @@ -23,18 +23,22 @@ import './theia-extra'; import './theia.proposed.canonicalUriProvider'; +import './theia.proposed.createFileSystemWatcher'; import './theia.proposed.customEditorMove'; +import './theia.proposed.debugVisualization'; import './theia.proposed.diffCommand'; import './theia.proposed.documentPaste'; -import './theia.proposed.dropMetadata'; import './theia.proposed.editSessionIdentityProvider'; import './theia.proposed.extensionsAny'; import './theia.proposed.externalUriOpener'; +import './theia.proposed.findTextInFiles'; +import './theia.proposed.fsChunks'; +import './theia.proposed.mappedEditsProvider'; +import './theia.proposed.multiDocumentHighlightProvider'; import './theia.proposed.notebookCellExecutionState'; import './theia.proposed.notebookKernelSource'; import './theia.proposed.notebookMessaging'; -import './theia.proposed.findTextInFiles'; -import './theia.proposed.fsChunks'; +import './theia.proposed.portsAttributes'; import './theia.proposed.profileContentHandlers'; import './theia.proposed.resolvers'; import './theia.proposed.scmValidation'; @@ -681,8 +685,12 @@ export module '@theia/plugin' { /** * Indicates that this markdown string is from a trusted source. Only *trusted* * markdown supports links that execute commands, e.g. `[Run it](command:myCommandId)`. + * + * Defaults to `false` (commands are disabled). + * + * If this is an object, only the set of commands listed in `enabledCommands` are allowed. */ - isTrusted?: boolean; + isTrusted?: boolean | { readonly enabledCommands: readonly string[] }; /** * Indicates that this markdown string can contain {@link ThemeIcon ThemeIcons}, e.g. `$(zap)`. @@ -1892,7 +1900,11 @@ export module '@theia/plugin' { /** * Render the line numbers with values relative to the primary cursor location. */ - Relative = 2 + Relative = 2, + /** + * Render the line numbers on every 10th line number. + */ + Interval = 3 } /** @@ -2731,6 +2743,12 @@ export module '@theia/plugin' { * Whether the current window is focused. */ readonly focused: boolean; + + /** + * Whether the window has been interacted with recently. This will change + * immediately on activity, or after a short time of user inactivity. + */ + readonly active: boolean; } /** @@ -3040,12 +3058,26 @@ export module '@theia/plugin' { */ readonly state: TerminalState; + /** + * An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered + * features for the terminal. This will always be `undefined` immediately after the terminal + * is created. Listen to {@link window.onDidChangeTerminalShellIntegration} to be notified + * when shell integration is activated for a terminal. + * + * Note that this object may remain undefined if shell integation never activates. For + * example Command Prompt does not support shell integration and a user's shell setup could + * conflict with the automatic shell integration activation. + * @stubbed + */ + readonly shellIntegration: TerminalShellIntegration | undefined; + /** * Send text to the terminal. - * @param text - text content. - * @param addNewLine - in case true - apply new line after the text, otherwise don't apply new line. This defaults to `true`. + * @param text - The text to send. + * @param shouldExecute - Indicates that the text being sent should be executed rather than just inserted in the terminal. + * The character added is \r, independent from the platform (compared to platform specific in vscode). This defaults to `true`. */ - sendText(text: string, addNewLine?: boolean): void; + sendText(text: string, shouldExecute?: boolean): void; /** * Show created terminal on the UI. @@ -3082,6 +3114,333 @@ export module '@theia/plugin' { readonly isInteractedWith: boolean; } + /** + * [Shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered capabilities owned by a terminal. + * @stubbed + */ + export interface TerminalShellIntegration { + /** + * The current working directory of the terminal. This {@link Uri} may represent a file on + * another machine (eg. ssh into another machine). This requires the shell integration to + * support working directory reporting. + * @stubbed + */ + readonly cwd: Uri | undefined; + + /** + * Execute a command, sending ^C as necessary to interrupt any running command if needed. + * + * @param commandLine The command line to execute, this is the exact text that will be sent + * to the terminal. + * + * @example + * // Execute a command in a terminal immediately after being created + * const myTerm = window.createTerminal(); + * window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration }) => { + * if (terminal === myTerm) { + * const execution = shellIntegration.executeCommand('echo "Hello world"'); + * window.onDidEndTerminalShellExecution(event => { + * if (event.execution === execution) { + * console.log(`Command exited with code ${event.exitCode}`); + * } + * } + * })); + * // Fallback to sendText if there is no shell integration within 3 seconds of launching + * setTimeout(() => { + * if (!myTerm.shellIntegration) { + * myTerm.sendText('echo "Hello world"'); + * // Without shell integration, we can't know when the command has finished or what the + * // exit code was. + * } + * }, 3000); + * + * @example + * // Send command to terminal that has been alive for a while + * const commandLine = 'echo "Hello world"'; + * if (term.shellIntegration) { + * const execution = shellIntegration.executeCommand({ commandLine }); + * window.onDidEndTerminalShellExecution(event => { + * if (event.execution === execution) { + * console.log(`Command exited with code ${event.exitCode}`); + * } + * } else { + * term.sendText(commandLine); + * // Without shell integration, we can't know when the command has finished or what the + * // exit code was. + * } + * @stubbed + */ + executeCommand(commandLine: string): TerminalShellExecution; + + /** + * Execute a command, sending ^C as necessary to interrupt any running command if needed. + * + * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) + * must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to + * verify whether it was successful. + * + * @param command A command to run. + * @param args Arguments to launch the executable with which will be automatically escaped + * based on the executable type. + * + * @example + * // Execute a command in a terminal immediately after being created + * const myTerm = window.createTerminal(); + * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => { + * if (terminal === myTerm) { + * const command = shellIntegration.executeCommand({ + * command: 'echo', + * args: ['Hello world'] + * }); + * const code = await command.exitCode; + * console.log(`Command exited with code ${code}`); + * } + * })); + * // Fallback to sendText if there is no shell integration within 3 seconds of launching + * setTimeout(() => { + * if (!myTerm.shellIntegration) { + * myTerm.sendText('echo "Hello world"'); + * // Without shell integration, we can't know when the command has finished or what the + * // exit code was. + * } + * }, 3000); + * + * @example + * // Send command to terminal that has been alive for a while + * const commandLine = 'echo "Hello world"'; + * if (term.shellIntegration) { + * const command = term.shellIntegration.executeCommand({ + * command: 'echo', + * args: ['Hello world'] + * }); + * const code = await command.exitCode; + * console.log(`Command exited with code ${code}`); + * } else { + * term.sendText(commandLine); + * // Without shell integration, we can't know when the command has finished or what the + * // exit code was. + * } + * @stubbed + */ + executeCommand(executable: string, args: string[]): TerminalShellExecution; + } + + /** + * A command that was executed in a terminal. + * @stubbed + */ + export interface TerminalShellExecution { + /** + * The command line that was executed. The {@link TerminalShellExecutionCommandLineConfidence confidence} + * of this value depends on the specific shell's shell integration implementation. This + * value may become more accurate after {@link window.onDidEndTerminalShellExecution} is + * fired. + * + * @example + * // Log the details of the command line on start and end + * window.onDidStartTerminalShellExecution(event => { + * const commandLine = event.execution.commandLine; + * console.log(`Command started\n${summarizeCommandLine(commandLine)}`); + * }); + * window.onDidEndTerminalShellExecution(event => { + * const commandLine = event.execution.commandLine; + * console.log(`Command ended\n${summarizeCommandLine(commandLine)}`); + * }); + * function summarizeCommandLine(commandLine: TerminalShellExecutionCommandLine) { + * return [ + * ` Command line: ${command.commandLine.value}`, + * ` Confidence: ${command.commandLine.confidence}`, + * ` Trusted: ${command.commandLine.isTrusted} + * ].join('\n'); + * } + * @stubbed + */ + readonly commandLine: TerminalShellExecutionCommandLine; + + /** + * The working directory that was reported by the shell when this command executed. This + * {@link Uri} may represent a file on another machine (eg. ssh into another machine). This + * requires the shell integration to support working directory reporting. + * @stubbed + */ + readonly cwd: Uri | undefined; + + /** + * Creates a stream of raw data (including escape sequences) that is written to the + * terminal. This will only include data that was written after `read` was called for + * the first time, ie. you must call `read` immediately after the command is executed via + * {@link TerminalShellIntegration.executeCommand} or + * {@link window.onDidStartTerminalShellExecution} to not miss any data. + * + * @example + * // Log all data written to the terminal for a command + * const command = term.shellIntegration.executeCommand({ commandLine: 'echo "Hello world"' }); + * const stream = command.read(); + * for await (const data of stream) { + * console.log(data); + * } + * @stubbed + */ + read(): AsyncIterable; + } + + /** + * A command line that was executed in a terminal. + * @stubbed + */ + export interface TerminalShellExecutionCommandLine { + /** + * The full command line that was executed, including both the command and its arguments. + * @stubbed + */ + readonly value: string; + + /** + * Whether the command line value came from a trusted source and is therefore safe to + * execute without user additional confirmation, such as a notification that asks "Do you + * want to execute (command)?". This verification is likely only needed if you are going to + * execute the command again. + * + * This is `true` only when the command line was reported explicitly by the shell + * integration script (ie. {@link TerminalShellExecutionCommandLineConfidence.High high confidence}) + * and it used a nonce for verification. + * @stubbed + */ + readonly isTrusted: boolean; + + /** + * The confidence of the command line value which is determined by how the value was + * obtained. This depends upon the implementation of the shell integration script. + * @stubbed + */ + readonly confidence: TerminalShellExecutionCommandLineConfidence; + } + + /** + * The confidence of a {@link TerminalShellExecutionCommandLine} value. + */ + enum TerminalShellExecutionCommandLineConfidence { + /** + * The command line value confidence is low. This means that the value was read from the + * terminal buffer using markers reported by the shell integration script. Additionally one + * of the following conditions will be met: + * + * - The command started on the very left-most column which is unusual, or + * - The command is multi-line which is more difficult to accurately detect due to line + * continuation characters and right prompts. + * - Command line markers were not reported by the shell integration script. + */ + Low = 0, + + /** + * The command line value confidence is medium. This means that the value was read from the + * terminal buffer using markers reported by the shell integration script. The command is + * single-line and does not start on the very left-most column (which is unusual). + */ + Medium = 1, + + /** + * The command line value confidence is high. This means that the value was explicitly sent + * from the shell integration script or the command was executed via the + * {@link TerminalShellIntegration.executeCommand} API. + */ + High = 2 + } + + /** + * An event signalling that a terminal's shell integration has changed. + * @stubbed + */ + export interface TerminalShellIntegrationChangeEvent { + /** + * The terminal that shell integration has been activated in. + * @stubbed + */ + readonly terminal: Terminal; + + /** + * The shell integration object. + * @stubbed + */ + readonly shellIntegration: TerminalShellIntegration; + } + + /** + * An event signalling that an execution has started in a terminal. + * @stubbed + */ + export interface TerminalShellExecutionStartEvent { + /** + * The terminal that shell integration has been activated in. + * @stubbed + */ + readonly terminal: Terminal; + + /** + * The shell integration object. + * @stubbed + */ + readonly shellIntegration: TerminalShellIntegration; + + /** + * The terminal shell execution that has ended. + * @stubbed + */ + readonly execution: TerminalShellExecution; + } + + /** + * An event signalling that an execution has ended in a terminal. + * @stubbed + */ + export interface TerminalShellExecutionEndEvent { + /** + * The terminal that shell integration has been activated in. + * @stubbed + */ + readonly terminal: Terminal; + + /** + * The shell integration object. + * @stubbed + */ + readonly shellIntegration: TerminalShellIntegration; + + /** + * The terminal shell execution that has ended. + * @stubbed + */ + readonly execution: TerminalShellExecution; + + /** + * The exit code reported by the shell. + * + * Note that `undefined` means the shell either did not report an exit code (ie. the shell + * integration script is misbehaving) or the shell reported a command started before the command + * finished (eg. a sub-shell was opened). Generally this should not happen, depending on the use + * case, it may be best to treat this as a failure. + * + * @example + * const execution = shellIntegration.executeCommand({ + * command: 'echo', + * args: ['Hello world'] + * }); + * window.onDidEndTerminalShellExecution(event => { + * if (event.execution === execution) { + * if (event.exitCode === undefined) { + * console.log('Command finished but exit code is unknown'); + * } else if (event.exitCode === 0) { + * console.log('Command succeeded'); + * } else { + * console.log('Command failed'); + * } + * } + * }); + * @stubbed + */ + readonly exitCode: number | undefined; + } + /** * Options to create terminal widget. */ @@ -3156,13 +3515,12 @@ export module '@theia/plugin' { /** * The icon path or {@link ThemeIcon} for the terminal. */ - iconPath?: ThemeIcon; + iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; /** * The icon {@link ThemeColor} for the terminal. * The `terminal.ansi*` theme keys are * recommended for the best contrast and consistency across themes. - * @stubbed */ color?: ThemeColor; } @@ -3277,7 +3635,7 @@ export module '@theia/plugin' { /** * The icon path or {@link ThemeIcon} for the terminal. */ - iconPath?: ThemeIcon; + iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; /** * The icon {@link ThemeColor} for the terminal. @@ -4012,6 +4370,14 @@ export module '@theia/plugin' { * The current `Extension` instance. */ readonly extension: Extension; + + /** + * An object that keeps information about how this extension can use language models. + * + * @see {@link LanguageModelChat.sendRequest} + * @stubbed + */ + readonly languageModelAccessInformation: LanguageModelAccessInformation; } /** @@ -5473,6 +5839,28 @@ export module '@theia/plugin' { */ export const onDidChangeTerminalState: Event; + /** + * Fires when shell integration activates or one of its properties changes in a terminal. + * @stubbed + */ + export const onDidChangeTerminalShellIntegration: Event; + + /** + * This will be fired when a terminal command is started. This event will fire only when + * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is + * activated for the terminal. + * @stubbed + */ + export const onDidStartTerminalShellExecution: Event; + + /** + * This will be fired when a terminal command is ended. This event will fire only when + * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is + * activated for the terminal. + * @stubbed + */ + export const onDidEndTerminalShellExecution: Event; + /** * Create new terminal with predefined options. * @param - terminal options. @@ -6341,13 +6729,31 @@ export module '@theia/plugin' { badge: ViewBadge | undefined; /** - * Reveal an element. By default revealed element is selected. + * Reveals the given element in the tree view. + * If the tree view is not visible then the tree view is shown and element is revealed. * + * By default revealed element is selected. * In order to not to select, set the option `select` to `false`. + * In order to focus, set the option `focus` to `true`. + * In order to expand the revealed element, set the option `expand` to `true`. To expand recursively set `expand` to the number of levels to expand. * - * **NOTE:** {@link TreeDataProvider TreeDataProvider} is required to implement {@link TreeDataProvider.getParent getParent} method to access this API. + * * *NOTE:* In VS Code, you can expand only to 3 levels maximum. This is not the case in Theia, there are no limits to expansion level. + * * *NOTE:* The {@link TreeDataProvider} that the `TreeView` {@link window.createTreeView is registered with} with must implement {@link TreeDataProvider.getParent getParent} method to access this API. */ - reveal(element: T, options?: { select?: boolean; focus?: boolean; expand?: boolean | number }): Thenable; + reveal(element: T, options?: { + /** + * If true, then the element will be selected. + */ + readonly select?: boolean; + /** + * If true, then the element will be focused. + */ + readonly focus?: boolean; + /** + * If true, then the element will be expanded. If a number is passed, then up to that number of levels of children will be expanded + */ + readonly expand?: boolean | number; + }): Thenable; } /** @@ -7526,6 +7932,29 @@ export module '@theia/plugin' { */ export function findFiles(include: GlobPattern, exclude?: GlobPattern | null, maxResults?: number, token?: CancellationToken): Thenable; + /** + * Saves the editor identified by the given resource and returns the resulting resource or `undefined` + * if save was not successful or no editor with the given resource was found. + * + * **Note** that an editor with the provided resource must be opened in order to be saved. + * + * @param uri the associated uri for the opened editor to save. + * @returns A thenable that resolves when the save operation has finished. + */ + export function save(uri: Uri): Thenable; + + /** + * Saves the editor identified by the given resource to a new file name as provided by the user and + * returns the resulting resource or `undefined` if save was not successful or cancelled or no editor + * with the given resource was found. + * + * **Note** that an editor with the provided resource must be opened in order to be saved as. + * + * @param uri the associated uri for the opened editor to save as. + * @returns A thenable that resolves when the save-as operation has finished. + */ + export function saveAs(uri: Uri): Thenable; + /** * Save all dirty files. * @@ -7564,7 +7993,7 @@ export module '@theia/plugin' { * @param options Immutable metadata about the provider. * @return A {@link Disposable disposable} that unregisters this provider when being disposed. */ - export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean, readonly isReadonly?: boolean }): Disposable; + export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean, readonly isReadonly?: boolean | MarkdownString }): Disposable; /** * Returns the {@link WorkspaceFolder workspace folder} that contains a given uri. @@ -7695,6 +8124,9 @@ export module '@theia/plugin' { /** * The application root folder from which the editor is running. + * + * *Note* that the value is the empty string when running in an + * environment that has no representation of an application root folder. */ export const appRoot: string; @@ -7876,7 +8308,7 @@ export module '@theia/plugin' { * @param pattern A file glob pattern like `*.{ts,js}` that will be matched on file paths * relative to the base path. */ - constructor(base: WorkspaceFolder | Uri | string, pattern: string) + constructor(base: WorkspaceFolder | Uri | string, pattern: string); } /** @@ -10252,15 +10684,7 @@ export module '@theia/plugin' { * @param uri A resource identifier. * @param edits An array of edits. */ - set(uri: Uri, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, WorkspaceEditEntryMetadata]>): void; - - /** - * Set (and replace) text edits or snippet edits with metadata for a resource. - * - * @param uri A resource identifier. - * @param edits An array of edits. - */ - set(uri: Uri, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, WorkspaceEditEntryMetadata]>): void; + set(uri: Uri, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, WorkspaceEditEntryMetadata | undefined]>): void; /** * Set (and replace) notebook edits for a resource. @@ -10276,7 +10700,7 @@ export module '@theia/plugin' { * @param uri A resource identifier. * @param edits An array of edits. */ - set(uri: Uri, edits: ReadonlyArray<[NotebookEdit, WorkspaceEditEntryMetadata]>): void; + set(uri: Uri, edits: ReadonlyArray<[NotebookEdit, WorkspaceEditEntryMetadata | undefined]>): void; /** * Get the text edits for a resource. @@ -12043,6 +12467,12 @@ export module '@theia/plugin' { * When true, the debug viewlet will not be automatically revealed for this session. */ suppressDebugView?: boolean; + /** + * Signals to the editor that the debug session was started from a test run + * request. This is used to link the lifecycle of the debug session and + * test run in UI actions. + */ + testRun?: TestRun; } /** @@ -12406,6 +12836,50 @@ export module '@theia/plugin' { Dynamic = 2 } + /** + * Represents a thread in a debug session. + */ + export class DebugThread { + /** + * Debug session for thread. + */ + readonly session: DebugSession; + + /** + * ID of the associated thread in the debug protocol. + */ + readonly threadId: number; + + /** + * @hidden + */ + private constructor(session: DebugSession, threadId: number); + } + + /** + * Represents a stack frame in a debug session. + */ + export class DebugStackFrame { + /** + * Debug session for thread. + */ + readonly session: DebugSession; + + /** + * ID of the associated thread in the debug protocol. + */ + readonly threadId: number; + /** + * ID of the stack frame in the debug protocol. + */ + readonly frameId: number; + + /** + * @hidden + */ + private constructor(session: DebugSession, threadId: number, frameId: number); + } + /** * Namespace for debug functionality. */ @@ -12455,6 +12929,19 @@ export module '@theia/plugin' { */ export const onDidChangeBreakpoints: Event; + /** + * The currently focused thread or stack frame, or `undefined` if no + * thread or stack is focused. A thread can be focused any time there is + * an active debug session, while a stack frame can only be focused when + * a session is paused and the call stack has been retrieved. + */ + export const activeStackItem: DebugThread | DebugStackFrame | undefined; + + /** + * An event which fires when the {@link debug.activeStackItem} has changed. + */ + export const onDidChangeActiveStackItem: Event; + /** * Register a {@link DebugAdapterDescriptorFactory debug adapter descriptor factory} for a specific debug type. * An extension is only allowed to register a DebugAdapterDescriptorFactory for the debug type(s) defined by the extension. Otherwise an error is thrown. @@ -13976,6 +14463,11 @@ export module '@theia/plugin' { * Note: you cannot use this option with any other options that prompt the user like {@link createIfNone}. */ silent?: boolean; + + /** + * The account that you would like to get a session for. This is passed down to the Authentication Provider to be used for creating the correct session. + */ + account?: AuthenticationSessionAccountInformation; } /** @@ -14036,6 +14528,18 @@ export module '@theia/plugin' { readonly changed: readonly AuthenticationSession[] | undefined; } + /** + * The options passed in to the {@link AuthenticationProvider.getSessions} and + * {@link AuthenticationProvider.createSession} call. + */ + export interface AuthenticationProviderSessionOptions { + /** + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. + */ + account?: AuthenticationSessionAccountInformation; + } + /** * A provider for performing authentication to a service. */ @@ -14050,9 +14554,10 @@ export module '@theia/plugin' { * Get a list of sessions. * @param scopes An optional list of scopes. If provided, the sessions returned should match * these permissions, otherwise all sessions should be returned. + * @param options Additional options for getting sessions. * @returns A promise that resolves to an array of authentication sessions. */ - getSessions(scopes?: readonly string[]): Thenable; + getSessions(scopes: readonly string[] | undefined, options: AuthenticationProviderSessionOptions): Thenable; /** * Prompts a user to login. @@ -14065,9 +14570,10 @@ export module '@theia/plugin' { * then this should never be called if there is already an existing session matching these * scopes. * @param scopes A list of scopes, permissions, that the new session should be created with. + * @param options Additional options for creating a session. * @returns A promise that resolves to an authentication session. */ - createSession(scopes: readonly string[]): Thenable; + createSession(scopes: readonly string[], options: AuthenticationProviderSessionOptions): Thenable; /** * Removes the session corresponding to session id. @@ -14127,6 +14633,20 @@ export module '@theia/plugin' { */ export function getSession(providerId: string, scopes: readonly string[], options?: AuthenticationGetSessionOptions): Thenable; + /** + * Get all accounts that the user is logged in to for the specified provider. + * Use this paired with {@link getSession} in order to get an authentication session for a specific account. + * + * Currently, there are only two authentication providers that are contributed from built in extensions + * to the editor that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. + * + * Note: Getting accounts does not imply that your extension has access to that account or its authentication sessions. You can verify access to the account by calling {@link getSession}. + * + * @param providerId The id of the provider to use + * @returns A thenable that resolves to a readonly array of authentication accounts. + */ + export function getAccounts(providerId: string): Thenable; + /** * An {@link Event event} which fires when the authentication sessions of an authentication provider have * been added, removed, or changed. @@ -15958,9 +16478,18 @@ export module '@theia/plugin' { * the generic "run all" button, then the default profile for * {@link TestRunProfileKind.Run} will be executed, although the * user can configure this. + * + * Changes the user makes in their default profiles will be reflected + * in this property after a {@link onDidChangeDefault} event. */ isDefault: boolean; + /** + * Fired when a user has changed whether this is a default profile. The + * event contains the new value of {@link isDefault} + */ + onDidChangeDefault: Event; + /** * Whether this profile supports continuous running of requests. If so, * then {@link TestRunRequest.continuous} may be set to `true`. Defaults @@ -16001,6 +16530,18 @@ export module '@theia/plugin' { */ runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void; + /** + * An extension-provided function that provides detailed statement and + * function-level coverage for a file. The editor will call this when more + * detail is needed for a file, such as when it's opened in an editor or + * expanded in the **Test Coverage** view. + * + * The {@link FileCoverage} object passed to this function is the same instance + * emitted on {@link TestRun.addCoverage} calls associated with this profile. + * @stubbed + */ + loadDetailedCoverage?: (testRun: TestRun, fileCoverage: FileCoverage, token: CancellationToken) => Thenable; + /** * Deletes the run profile. */ @@ -16191,12 +16732,21 @@ export module '@theia/plugin' { readonly continuous?: boolean; /** - * @param include Array of specific tests to run, or undefined to run all tests + * Controls how test Test Results view is focused. If true, the editor + * will keep the maintain the user's focus. If false, the editor will + * prefer to move focus into the Test Results view, although + * this may be configured by users. + */ + readonly preserveFocus: boolean; + + /** + * @param include Array of specific tests to run, or undefined to run all tests * @param exclude An array of tests to exclude from the run. * @param profile The run profile used for this request. * @param continuous Whether to run tests continuously as source changes. + * @param preserveFocus Whether to preserve the user's focus when the run is started */ - constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); + constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean, preserveFocus?: boolean); } /** @@ -16280,11 +16830,23 @@ export module '@theia/plugin' { */ appendOutput(output: string, location?: Location, test?: TestItem): void; + /** + * Adds coverage for a file in the run. + * @stubbed + */ + addCoverage(fileCoverage: FileCoverage): void; + /** * Signals the end of the test run. Any tests included in the run whose * states have not been updated will have their state reset. */ end(): void; + + /** + * An event fired when the editor is no longer interested in data + * associated with the test run. + */ + onDidDispose: Event; } /** @@ -16423,6 +16985,34 @@ export module '@theia/plugin' { error: string | MarkdownString | undefined; } + /** + * A stack frame found in the {@link TestMessage.stackTrace}. + */ + export class TestMessageStackFrame { + /** + * The location of this stack frame. This should be provided as a URI if the + * location of the call frame can be accessed by the editor. + */ + uri?: Uri; + + /** + * Position of the stack frame within the file. + */ + position?: Position; + + /** + * The name of the stack frame, typically a method or function name. + */ + label: string; + + /** + * @param label The name of the stack frame + * @param file The file URI of the stack frame + * @param position The position of the stack frame within the file + */ + constructor(label: string, uri?: Uri, position?: Position); + } + /** * Message associated with the test state. Can be linked to a specific * source range -- useful for assertion failures, for example. @@ -16448,6 +17038,42 @@ export module '@theia/plugin' { */ location?: Location; + /** + * Context value of the test item. This can be used to contribute message- + * specific actions to the test peek view. The value set here can be found + * in the `testMessage` property of the following `menus` contribution points: + * + * - `testing/message/context` - context menu for the message in the results tree + * - `testing/message/content` - a prominent button overlaying editor content where + * the message is displayed. + * + * For example: + * + * ```json + * "contributes": { + * "menus": { + * "testing/message/content": [ + * { + * "command": "extension.deleteCommentThread", + * "when": "testMessage == canApplyRichDiff" + * } + * ] + * } + * } + * ``` + * + * The command will be called with an object containing: + * - `test`: the {@link TestItem} the message is associated with, *if* it + * is still present in the {@link TestController.items} collection. + * - `message`: the {@link TestMessage} instance. + */ + contextValue?: string; + + /** + * The stack trace associated with the message or failure. + */ + stackTrace?: TestMessageStackFrame[]; + /** * Creates a new TestMessage that will present as a diff in the editor. * @param message Message to display to the user. @@ -16462,6 +17088,1029 @@ export module '@theia/plugin' { */ constructor(message: string | MarkdownString); } + + /** + * A class that contains information about a covered resource. A count can + * be give for lines, branches, and declarations in a file. + */ + export class TestCoverageCount { + /** + * Number of items covered in the file. + */ + covered: number; + /** + * Total number of covered items in the file. + */ + total: number; + + /** + * @param covered Value for {@link TestCoverageCount.covered} + * @param total Value for {@link TestCoverageCount.total} + */ + constructor(covered: number, total: number); + } + + /** + * Contains coverage metadata for a file. + */ + export class FileCoverage { + /** + * File URI. + */ + readonly uri: Uri; + + /** + * Statement coverage information. If the reporter does not provide statement + * coverage information, this can instead be used to represent line coverage. + */ + statementCoverage: TestCoverageCount; + + /** + * Branch coverage information. + */ + branchCoverage?: TestCoverageCount; + + /** + * Declaration coverage information. Depending on the reporter and + * language, this may be types such as functions, methods, or namespaces. + */ + declarationCoverage?: TestCoverageCount; + + /** + * Creates a {@link FileCoverage} instance with counts filled in from + * the coverage details. + * @param uri Covered file URI + * @param detailed Detailed coverage information + */ + static fromDetails(uri: Uri, details: readonly FileCoverageDetail[]): FileCoverage; + + /** + * @param uri Covered file URI + * @param statementCoverage Statement coverage information. If the reporter + * does not provide statement coverage information, this can instead be + * used to represent line coverage. + * @param branchCoverage Branch coverage information + * @param declarationCoverage Declaration coverage information + */ + constructor( + uri: Uri, + statementCoverage: TestCoverageCount, + branchCoverage?: TestCoverageCount, + declarationCoverage?: TestCoverageCount, + ); + } + + /** + * Contains coverage information for a single statement or line. + */ + export class StatementCoverage { + /** + * The number of times this statement was executed, or a boolean indicating + * whether it was executed if the exact count is unknown. If zero or false, + * the statement will be marked as un-covered. + */ + executed: number | boolean; + + /** + * Statement location. + */ + location: Position | Range; + + /** + * Coverage from branches of this line or statement. If it's not a + * conditional, this will be empty. + */ + branches: BranchCoverage[]; + + /** + * @param location The statement position. + * @param executed The number of times this statement was executed, or a + * boolean indicating whether it was executed if the exact count is + * unknown. If zero or false, the statement will be marked as un-covered. + * @param branches Coverage from branches of this line. If it's not a + * conditional, this should be omitted. + */ + constructor(executed: number | boolean, location: Position | Range, branches?: BranchCoverage[]); + } + + /** + * Contains coverage information for a branch of a {@link StatementCoverage}. + */ + export class BranchCoverage { + /** + * The number of times this branch was executed, or a boolean indicating + * whether it was executed if the exact count is unknown. If zero or false, + * the branch will be marked as un-covered. + */ + executed: number | boolean; + + /** + * Branch location. + */ + location?: Position | Range; + + /** + * Label for the branch, used in the context of "the ${label} branch was + * not taken," for example. + */ + label?: string; + + /** + * @param executed The number of times this branch was executed, or a + * boolean indicating whether it was executed if the exact count is + * unknown. If zero or false, the branch will be marked as un-covered. + * @param location The branch position. + */ + constructor(executed: number | boolean, location?: Position | Range, label?: string); + } + + /** + * Contains coverage information for a declaration. Depending on the reporter + * and language, this may be types such as functions, methods, or namespaces. + */ + export class DeclarationCoverage { + /** + * Name of the declaration. + */ + name: string; + + /** + * The number of times this declaration was executed, or a boolean + * indicating whether it was executed if the exact count is unknown. If + * zero or false, the declaration will be marked as un-covered. + */ + executed: number | boolean; + + /** + * Declaration location. + */ + location: Position | Range; + + /** + * @param executed The number of times this declaration was executed, or a + * boolean indicating whether it was executed if the exact count is + * unknown. If zero or false, the declaration will be marked as un-covered. + * @param location The declaration position. + */ + constructor(name: string, executed: number | boolean, location: Position | Range); + } + + /** + * Coverage details returned from {@link TestRunProfile.loadDetailedCoverage}. + */ + export type FileCoverageDetail = StatementCoverage | DeclarationCoverage; + + /** + * Represents a user request in chat history. + */ + export class ChatRequestTurn { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequestTurn.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The id of the chat participant to which this request was directed. + */ + readonly participant: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command?: string; + + /** + * The references that were used in this message. + */ + readonly references: ChatPromptReference[]; + + /** + * @hidden + */ + private constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string); + } + + /** + * Represents a chat participant's response in chat history. + */ + export class ChatResponseTurn { + /** + * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. + */ + readonly response: ReadonlyArray; + + /** + * The result that was received from the chat participant. + */ + readonly result: ChatResult; + + /** + * The id of the chat participant that this response came from. + */ + readonly participant: string; + + /** + * The name of the command that this response came from. + */ + readonly command?: string; + + /** + * @hidden + */ + private constructor(response: ReadonlyArray, result: ChatResult, participant: string); + } + + /** + * Extra context passed to a participant. + */ + export interface ChatContext { + /** + * All of the chat messages so far in the current chat session. Currently, only chat messages for the current participant are included. + */ + readonly history: ReadonlyArray; + } + + /** + * Represents an error result from a chat request. + */ + export interface ChatErrorDetails { + /** + * An error message that is shown to the user. + */ + message: string; + + /** + * If set to true, the response will be partly blurred out. + */ + responseIsFiltered?: boolean; + } + + /** + * The result of a chat request. + */ + export interface ChatResult { + /** + * If the request resulted in an error, this property defines the error details. + */ + errorDetails?: ChatErrorDetails; + + /** + * Arbitrary metadata for this result. Can be anything, but must be JSON-stringifyable. + */ + readonly metadata?: { readonly [key: string]: any }; + } + + /** + * Represents the type of user feedback received. + */ + export enum ChatResultFeedbackKind { + /** + * The user marked the result as unhelpful. + */ + Unhelpful = 0, + + /** + * The user marked the result as helpful. + */ + Helpful = 1, + } + + /** + * Represents user feedback for a result. + */ + export interface ChatResultFeedback { + /** + * The ChatResult for which the user is providing feedback. + * This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + */ + readonly result: ChatResult; + + /** + * The kind of feedback that was received. + */ + readonly kind: ChatResultFeedbackKind; + } + + /** + * A followup question suggested by the participant. + */ + export interface ChatFollowup { + /** + * The message to send to the chat. + */ + prompt: string; + + /** + * A title to show the user. The prompt will be shown by default, when this is unspecified. + */ + label?: string; + + /** + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID. + * Followups can only invoke a participant that was contributed by the same extension. + */ + participant?: string; + + /** + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different command. + */ + command?: string; + } + + /** + * Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat. + */ + export interface ChatFollowupProvider { + /** + * Provide followups for the given result. + * @param result This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + * @param token A cancellation token. + */ + provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; + } + + /** + * A chat request handler is a callback that will be invoked when a request is made to a chat participant. + */ + export type ChatRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; + + /** + * A chat participant can be invoked by the user in a chat session, using the `@` prefix. When it is invoked, it handles the chat request and is solely + * responsible for providing a response to the user. A ChatParticipant is created using {@link chat.createChatParticipant}. + */ + export interface ChatParticipant { + /** + * A unique ID for this participant. + */ + readonly id: string; + + /** + * An icon for the participant shown in UI. + */ + iconPath?: Uri | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } | ThemeIcon; + + /** + * The handler for requests to this participant. + */ + requestHandler: ChatRequestHandler; + + /** + * This provider will be called once after each request to retrieve suggested followup questions. + */ + followupProvider?: ChatFollowupProvider; + + /** + * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes + * a result. + * + * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was + * previously returned from this chat participant. + */ + onDidReceiveFeedback: Event; + + /** + * Dispose this participant and free resources. + */ + dispose(): void; + } + + /** + * A reference to a value that the user added to their chat request. + */ + export interface ChatPromptReference { + /** + * A unique identifier for this kind of reference. + */ + readonly id: string; + + /** + * The start and end index of the reference in the {@link ChatRequest.prompt prompt}. When undefined, the reference was not part of the prompt text. + * + * *Note* that the indices take the leading `#`-character into account which means they can + * used to modify the prompt as-is. + */ + readonly range?: [start: number, end: number]; + + /** + * A description of this value that could be used in an LLM prompt. + */ + readonly modelDescription?: string; + + /** + * The value of this reference. The `string | Uri | Location` types are used today, but this could expand in the future. + */ + readonly value: string | Uri | Location | unknown; + } + + /** + * A request to a chat participant. + */ + export interface ChatRequest { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequest.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command: string | undefined; + + /** + * The list of references and their values that are referenced in the prompt. + * + * *Note* that the prompt contains references as authored and that it is up to the participant + * to further modify the prompt, for instance by inlining reference values or creating links to + * headings which contain the resolved values. References are sorted in reverse by their range + * in the prompt. That means the last reference in the prompt is the first in this list. This simplifies + * string-manipulation of the prompt. + */ + readonly references: readonly ChatPromptReference[]; + } + + /** + * The ChatResponseStream is how a participant is able to return content to the chat view. It provides several methods for streaming different types of content + * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it + * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. + */ + export interface ChatResponseStream { + /** + * Push a markdown part to this stream. Short-hand for + * `push(new ChatResponseMarkdownPart(value))`. + * + * @see {@link ChatResponseStream.push} + * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. + */ + markdown(value: string | MarkdownString): void; + + /** + * Push an anchor part to this stream. Short-hand for + * `push(new ChatResponseAnchorPart(value, title))`. + * An anchor is an inline reference to some type of resource. + * + * @param value A uri, location, or symbol information. + * @param title An optional title that is rendered with value. + */ + anchor(value: Uri | Location, title?: string): void; + + /** + * Push a command button part to this stream. Short-hand for + * `push(new ChatResponseCommandButtonPart(value, title))`. + * + * @param command A Command that will be executed when the button is clicked. + */ + button(command: Command): void; + + /** + * Push a filetree part to this stream. Short-hand for + * `push(new ChatResponseFileTreePart(value))`. + * + * @param value File tree data. + * @param baseUri The base uri to which this file tree is relative. + */ + filetree(value: ChatResponseFileTree[], baseUri: Uri): void; + + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + */ + progress(value: string): void; + + /** + * Push a reference to this stream. Short-hand for + * `push(new ChatResponseReferencePart(value))`. + * + * *Note* that the reference is not rendered inline with the response. + * + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ + reference(value: Uri | Location, iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }): void; + + /** + * Pushes a part to this stream. + * + * @param part A response part, rendered or metadata + */ + push(part: ChatResponsePart): void; + } + + /** + * Represents a part of a chat response that is formatted as Markdown. + */ + export class ChatResponseMarkdownPart { + /** + * A markdown string or a string that should be interpreted as markdown. + */ + value: MarkdownString; + + /** + * Create a new ChatResponseMarkdownPart. + * + * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. + */ + constructor(value: string | MarkdownString); + } + + /** + * Represents a file tree structure in a chat response. + */ + export interface ChatResponseFileTree { + /** + * The name of the file or directory. + */ + name: string; + + /** + * An array of child file trees, if the current file tree is a directory. + */ + children?: ChatResponseFileTree[]; + } + + /** + * Represents a part of a chat response that is a file tree. + */ + export class ChatResponseFileTreePart { + /** + * File tree data. + */ + value: ChatResponseFileTree[]; + + /** + * The base uri to which this file tree is relative + */ + baseUri: Uri; + + /** + * Create a new ChatResponseFileTreePart. + * @param value File tree data. + * @param baseUri The base uri to which this file tree is relative. + */ + constructor(value: ChatResponseFileTree[], baseUri: Uri); + } + + /** + * Represents a part of a chat response that is an anchor, that is rendered as a link to a target. + */ + export class ChatResponseAnchorPart { + /** + * The target of this anchor. + */ + value: Uri | Location; + + /** + * An optional title that is rendered with value. + */ + title?: string; + + /** + * Create a new ChatResponseAnchorPart. + * @param value A uri or location. + * @param title An optional title that is rendered with value. + */ + constructor(value: Uri | Location, title?: string); + } + + /** + * Represents a part of a chat response that is a progress message. + */ + export class ChatResponseProgressPart { + /** + * The progress message + */ + value: string; + + /** + * Create a new ChatResponseProgressPart. + * @param value A progress message + */ + constructor(value: string); + } + + /** + * Represents a part of a chat response that is a reference, rendered separately from the content. + */ + export class ChatResponseReferencePart { + /** + * The reference target. + */ + value: Uri | Location; + + /** + * The icon for the reference. + */ + iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }; + + /** + * Create a new ChatResponseReferencePart. + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ + constructor(value: Uri | Location, iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }); + } + + /** + * Represents a part of a chat response that is a button that executes a command. + */ + export class ChatResponseCommandButtonPart { + /** + * The command that will be executed when the button is clicked. + */ + value: Command; + + /** + * Create a new ChatResponseCommandButtonPart. + * @param value A Command that will be executed when the button is clicked. + */ + constructor(value: Command); + } + + /** + * Represents the different chat response types. + */ + export type ChatResponsePart = ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart + | ChatResponseProgressPart | ChatResponseReferencePart | ChatResponseCommandButtonPart; + + /** + * Namespace for chat functionality. Users interact with chat participants by sending messages + * to them in the chat view. Chat participants can respond with markdown or other types of content + * via the {@link ChatResponseStream}. + */ + export namespace chat { + /** + * Create a new {@link ChatParticipant chat participant} instance. + * + * @param id A unique identifier for the participant. + * @param handler A request handler for the participant. + * @returns A new chat participant + * @stubbed + */ + export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant; + } + + /** + * Represents the role of a chat message. This is either the user or the assistant. + */ + export enum LanguageModelChatMessageRole { + /** + * The user role, e.g the human interacting with a language model. + */ + User = 1, + + /** + * The assistant role, e.g. the language model generating responses. + */ + Assistant = 2 + } + + /** + * Represents a message in a chat. Can assume different roles, like user or assistant. + */ + export class LanguageModelChatMessage { + + /** + * Utility to create a new user message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static User(content: string, name?: string): LanguageModelChatMessage; + + /** + * Utility to create a new assistant message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static Assistant(content: string, name?: string): LanguageModelChatMessage; + + /** + * The role of this message. + */ + role: LanguageModelChatMessageRole; + + /** + * The content of this message. + */ + content: string; + + /** + * The optional name of a user for this message. + */ + name: string | undefined; + + /** + * Create a new user message. + * + * @param role The role of the message. + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + constructor(role: LanguageModelChatMessageRole, content: string, name?: string); + } + + /** + * Represents a language model response. + * + * @see {@link LanguageModelAccess.chatRequest} + */ + export interface LanguageModelChatResponse { + + /** + * An async iterable that is a stream of text chunks forming the overall response. + * + * *Note* that this stream will error when during data receiving an error occurs. Consumers of + * the stream should handle the errors accordingly. + * + * To cancel the stream, the consumer can {@link CancellationTokenSource.cancel cancel} the token that was used to make the request + * or break from the for-loop. + * + * @example + * ```ts + * try { + * // consume stream + * for await (const chunk of response.text) { + * console.log(chunk); + * } + * + * } catch(e) { + * // stream ended with an error + * console.error(e); + * } + * ``` + */ + text: AsyncIterable; + } + + /** + * Represents a language model for making chat requests. + * + * @see {@link lm.selectChatModels} + */ + export interface LanguageModelChat { + + /** + * Human-readable name of the language model. + */ + readonly name: string; + + /** + * Opaque identifier of the language model. + */ + readonly id: string; + + /** + * A well-known identifier of the vendor of the language model. An example is `copilot`, but + * values are defined by extensions contributing chat models and need to be looked up with them. + */ + readonly vendor: string; + + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + readonly family: string; + + /** + * Opaque version string of the model. This is defined by the extension contributing the language model + * and subject to change. + */ + readonly version: string; + + /** + * The maximum number of tokens that can be sent to the model in a single request. + */ + readonly maxInputTokens: number; + + /** + * Make a chat request using a language model. + * + * *Note* that language model use may be subject to access restrictions and user consent. Calling this function + * for the first time (for a extension) will show a consent dialog to the user and because of that this function + * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} + * to check if they have the necessary permissions to make a request. + * + * This function will return a rejected promise if making a request to the language model is not + * possible. Reasons for this can be: + * + * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} + * - model does not exist anymore, see {@link LanguageModelError.NotFound `NotFound`} + * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} + * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} + * + * @param messages An array of message instances. + * @param options Options that control the request. + * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. + * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. + */ + sendRequest(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + + /** + * Count the number of tokens in a message using the model specific tokenizer-logic. + * @param text A string or a message instance. + * @param token Optional cancellation token. See {@link CancellationTokenSource} for how to create one. + * @returns A thenable that resolves to the number of tokens. + */ + countTokens(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + } + + /** + * Describes how to select language models for chat requests. + * + * @see {@link lm.selectChatModels} + */ + export interface LanguageModelChatSelector { + + /** + * A vendor of language models. + * @see {@link LanguageModelChat.vendor} + */ + vendor?: string; + + /** + * A family of language models. + * @see {@link LanguageModelChat.family} + */ + family?: string; + + /** + * The version of a language model. + * @see {@link LanguageModelChat.version} + */ + version?: string; + + /** + * The identifier of a language model. + * @see {@link LanguageModelChat.id} + */ + id?: string; + } + + /** + * An error type for language model specific errors. + * + * Consumers of language models should check the code property to determine specific + * failure causes, like `if(someError.code === vscode.LanguageModelError.NotFound.name) {...}` + * for the case of referring to an unknown language model. For unspecified errors the `cause`-property + * will contain the actual error. + */ + export class LanguageModelError extends Error { + + /** + * The requestor does not have permissions to use this + * language model + */ + static NoPermissions(message?: string): LanguageModelError; + + /** + * The requestor is blocked from using this language model. + */ + static Blocked(message?: string): LanguageModelError; + + /** + * The language model does not exist. + */ + static NotFound(message?: string): LanguageModelError; + + /** + * A code that identifies this error. + * + * Possible values are names of errors, like {@linkcode LanguageModelError.NotFound NotFound}, + * or `Unknown` for unspecified errors from the language model itself. In the latter case the + * `cause`-property will contain the actual error. + */ + readonly code: string; + } + + /** + * Options for making a chat request using a language model. + * + * @see {@link LanguageModelChat.sendRequest} + */ + export interface LanguageModelChatRequestOptions { + + /** + * A human-readable message that explains why access to a language model is needed and what feature is enabled by it. + */ + justification?: string; + + /** + * A set of options that control the behavior of the language model. These options are specific to the language model + * and need to be lookup in the respective documentation. + */ + modelOptions?: { [name: string]: any }; + } + + /** + * Namespace for language model related functionality. + */ + export namespace lm { + + /** + * An event that is fired when the set of available chat models changes. + * @stubbed + */ + export const onDidChangeChatModels: Event; + + /** + * Select chat models by a {@link LanguageModelChatSelector selector}. This can yield multiple or no chat models and + * extensions must handle these cases, esp. when no chat model exists, gracefully. + * + * ```ts + * const models = await vscode.lm.selectChatModels({ family: 'gpt-3.5-turbo' }); + * if (models.length > 0) { + * const [first] = models; + * const response = await first.sendRequest(...) + * // ... + * } else { + * // NO chat models available + * } + * ``` + * + * A selector can be written to broadly match all models of a given vendor or family, or it can narrowly select one model by ID. + * Keep in mind that the available set of models will change over time, but also that prompts may perform differently in + * different models. + * + * *Note* that extensions can hold on to the results returned by this function and use them later. However, when the + * {@link onDidChangeChatModels}-event is fired the list of chat models might have changed and extensions should re-query. + * + * @param selector A chat model selector. When omitted all chat models are returned. + * @returns An array of chat models, can be empty! + * @stubbed + */ + export function selectChatModels(selector?: LanguageModelChatSelector): Thenable; + } + + /** + * Represents extension specific information about the access to language models. + */ + export interface LanguageModelAccessInformation { + + /** + * An event that fires when access information changes. + */ + onDidChange: Event; + + /** + * Checks if a request can be made to a language model. + * + * *Note* that calling this function will not trigger a consent UI but just checks for a persisted state. + * + * @param chat A language model chat object. + * @return `true` if a request can be made, `false` if not, `undefined` if the language + * model does not exist or consent hasn't been asked for. + */ + canSendRequest(chat: LanguageModelChat): boolean | undefined; + } + /** * Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise, * and others. This API makes no assumption about what promise library is being used which diff --git a/packages/plugin/src/theia.proposed.createFileSystemWatcher.ts b/packages/plugin/src/theia.proposed.createFileSystemWatcher.ts new file mode 100644 index 0000000000000..17d4d641266b7 --- /dev/null +++ b/packages/plugin/src/theia.proposed.createFileSystemWatcher.ts @@ -0,0 +1,65 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// code copied and modified from https://github.com/microsoft/vscode/blob/release/1.93/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts + +declare module '@theia/plugin' { + + export interface FileSystemWatcherOptions { + + /** + * Ignore when files have been created. + */ + readonly ignoreCreateEvents?: boolean; + + /** + * Ignore when files have been changed. + */ + readonly ignoreChangeEvents?: boolean; + + /** + * Ignore when files have been deleted. + */ + readonly ignoreDeleteEvents?: boolean; + + /** + * An optional set of glob patterns to exclude from watching. + * Glob patterns are always matched relative to the watched folder. + */ + readonly excludes: string[]; + } + + export namespace workspace { + + /** + * A variant of {@link workspace.createFileSystemWatcher} that optionally allows to specify + * a set of glob patterns to exclude from watching. + * + * It provides the following advantages over the other {@link workspace.createFileSystemWatcher} + * method: + * - the configured excludes from `files.watcherExclude` setting are NOT applied + * - requests for recursive file watchers inside the opened workspace are NOT ignored + * - the watcher is ONLY notified for events from this request and not from any other watcher + * + * As such, this method is prefered in cases where you want full control over the watcher behavior + * without being impacted by settings or other watchers that are installed. + */ + export function createFileSystemWatcher(pattern: RelativePattern, options?: FileSystemWatcherOptions): FileSystemWatcher; + } +} diff --git a/packages/plugin/src/theia.proposed.debugVisualization.d.ts b/packages/plugin/src/theia.proposed.debugVisualization.d.ts new file mode 100644 index 0000000000000..181fda82930bf --- /dev/null +++ b/packages/plugin/src/theia.proposed.debugVisualization.d.ts @@ -0,0 +1,189 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +declare module '@theia/plugin' { + export namespace debug { + /** + * Registers a custom data visualization for variables when debugging. + * + * @param id The corresponding ID in the package.json `debugVisualizers` contribution point. + * @param provider The {@link DebugVisualizationProvider} to register + * @stubbed + */ + export function registerDebugVisualizationProvider( + id: string, + provider: DebugVisualizationProvider + ): Disposable; + + /** + * Registers a tree that can be referenced by {@link DebugVisualization.visualization}. + * @param id + * @param provider + * @stubbed + */ + export function registerDebugVisualizationTreeProvider( + id: string, + provider: DebugVisualizationTree + ): Disposable; + } + + /** + * An item from the {@link DebugVisualizationTree} + */ + export interface DebugTreeItem { + /** + * A human-readable string describing this item. + */ + label: string; + + /** + * A human-readable string which is rendered less prominent. + */ + description?: string; + + /** + * {@link TreeItemCollapsibleState} of the tree item. + */ + collapsibleState?: TreeItemCollapsibleState; + + /** + * Context value of the tree item. This can be used to contribute item specific actions in the tree. + * For example, a tree item is given a context value as `folder`. When contributing actions to `view/item/context` + * using `menus` extension point, you can specify context value for key `viewItem` in `when` expression like `viewItem == folder`. + * ```json + * "contributes": { + * "menus": { + * "view/item/context": [ + * { + * "command": "extension.deleteFolder", + * "when": "viewItem == folder" + * } + * ] + * } + * } + * ``` + * This will show action `extension.deleteFolder` only for items with `contextValue` is `folder`. + */ + contextValue?: string; + + /** + * Whether this item can be edited by the user. + */ + canEdit?: boolean; + } + + /** + * Provides a tree that can be referenced in debug visualizations. + */ + export interface DebugVisualizationTree { + /** + * Gets the tree item for an element or the base context item. + */ + getTreeItem(context: DebugVisualizationContext): ProviderResult; + /** + * Gets children for the tree item or the best context item. + */ + getChildren(element: T): ProviderResult; + /** + * Handles the user editing an item. + */ + editItem?(item: T, value: string): ProviderResult; + } + + export class DebugVisualization { + /** + * The name of the visualization to show to the user. + */ + name: string; + + /** + * An icon for the view when it's show in inline actions. + */ + iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + + /** + * Visualization to use for the variable. This may be either: + * - A command to run when the visualization is selected for a variable. + * - A reference to a previously-registered {@link DebugVisualizationTree} + */ + visualization?: Command | { treeId: string }; + + /** + * Creates a new debug visualization object. + * @param name Name of the visualization to show to the user. + */ + constructor(name: string); + } + + export interface DebugVisualizationProvider { + /** + * Called for each variable when the debug session stops. It should return + * any visualizations the extension wishes to show to the user. + * + * Note that this is only called when its `when` clause defined under the + * `debugVisualizers` contribution point in the `package.json` evaluates + * to true. + */ + provideDebugVisualization(context: DebugVisualizationContext, token: CancellationToken): ProviderResult; + + /** + * Invoked for a variable when a user picks the visualizer. + * + * It may return a {@link TreeView} that's shown in the Debug Console or + * inline in a hover. A visualizer may choose to return `undefined` from + * this function and instead trigger other actions in the UI, such as opening + * a custom {@link WebviewView}. + */ + resolveDebugVisualization?(visualization: T, token: CancellationToken): ProviderResult; + } + + export interface DebugVisualizationContext { + /** + * The Debug Adapter Protocol Variable to be visualized. + * @see https://microsoft.github.io/debug-adapter-protocol/specification#Types_Variable + */ + variable: any; + /** + * The Debug Adapter Protocol variable reference the type (such as a scope + * or another variable) that contained this one. Empty for variables + * that came from user evaluations in the Debug Console. + * @see https://microsoft.github.io/debug-adapter-protocol/specification#Types_Variable + */ + containerId?: number; + /** + * The ID of the Debug Adapter Protocol StackFrame in which the variable was found, + * for variables that came from scopes in a stack frame. + * @see https://microsoft.github.io/debug-adapter-protocol/specification#Types_StackFrame + */ + frameId?: number; + /** + * The ID of the Debug Adapter Protocol Thread in which the variable was found. + * @see https://microsoft.github.io/debug-adapter-protocol/specification#Types_StackFrame + */ + threadId: number; + /** + * The debug session the variable belongs to. + */ + session: DebugSession; + } +} diff --git a/packages/plugin/src/theia.proposed.documentPaste.d.ts b/packages/plugin/src/theia.proposed.documentPaste.d.ts index 2289ab1686f6a..be606313530bb 100644 --- a/packages/plugin/src/theia.proposed.documentPaste.d.ts +++ b/packages/plugin/src/theia.proposed.documentPaste.d.ts @@ -23,57 +23,154 @@ export module '@theia/plugin' { /** - * Provider invoked when the user copies and pastes code. + * Identifies a {@linkcode DocumentDropEdit} or {@linkcode DocumentPasteEdit} */ - export interface DocumentPasteEditProvider { + class DocumentDropOrPasteEditKind { + static readonly Empty: DocumentDropOrPasteEditKind; + + private constructor(value: string); + + /** + * The raw string value of the kind. + */ + readonly value: string; + + /** + * Create a new kind by appending additional scopes to the current kind. + * + * Does not modify the current kind. + */ + append(...parts: string[]): DocumentDropOrPasteEditKind; /** - * Optional method invoked after the user copies text in a file. + * Checks if this kind intersects `other`. * - * During {@link prepareDocumentPaste}, an extension can compute metadata that is attached to - * a {@link DataTransfer} and is passed back to the provider in {@link provideDocumentPasteEdits}. + * The kind `"text.plain"` for example intersects `text`, `"text.plain"` and `"text.plain.list"`, + * but not `"unicorn"`, or `"textUnicorn.plain"`. * - * @param document Document where the copy took place. - * @param ranges Ranges being copied in the `document`. - * @param dataTransfer The data transfer associated with the copy. You can store additional values on this for later use in {@link provideDocumentPasteEdits}. + * @param other Kind to check. + */ + intersects(other: DocumentDropOrPasteEditKind): boolean; + + /** + * Checks if `other` is a sub-kind of this `DocumentDropOrPasteEditKind`. + * + * The kind `"text.plain"` for example contains `"text.plain"` and `"text.plain.list"`, + * but not `"text"` or `"unicorn.text.plain"`. + * + * @param other Kind to check. + */ + contains(other: DocumentDropOrPasteEditKind): boolean; + } + + /** + * The reason why paste edits were requested. + */ + export enum DocumentPasteTriggerKind { + /** + * Pasting was requested as part of a normal paste operation. + */ + Automatic = 0, + + /** + * Pasting was requested by the user with the `paste as` command. + */ + PasteAs = 1, + } + + /** + * Additional information about the paste operation. + */ + + export interface DocumentPasteEditContext { + /** + * Requested kind of paste edits to return. + */ + readonly only: DocumentDropOrPasteEditKind | undefined; + + /** + * The reason why paste edits were requested. + */ + readonly triggerKind: DocumentPasteTriggerKind; + } + + /** + * Provider invoked when the user copies or pastes in a {@linkcode TextDocument}. + */ + interface DocumentPasteEditProvider { + + /** + * Optional method invoked after the user copies from a {@link TextEditor text editor}. + * + * This allows the provider to attach metadata about the copied text to the {@link DataTransfer}. This data + * transfer is then passed back to providers in {@linkcode provideDocumentPasteEdits}. + * + * Note that currently any changes to the {@linkcode DataTransfer} are isolated to the current editor window. + * This means that any added metadata cannot be seen by other editor windows or by other applications. + * + * @param document Text document where the copy took place. + * @param ranges Ranges being copied in {@linkcode document}. + * @param dataTransfer The data transfer associated with the copy. You can store additional values on this for + * later use in {@linkcode provideDocumentPasteEdits}. This object is only valid for the duration of this method. * @param token A cancellation token. + * + * @return Optional thenable that resolves when all changes to the `dataTransfer` are complete. */ prepareDocumentPaste?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, token: CancellationToken): void | Thenable; /** - * Invoked before the user pastes into a document. + * Invoked before the user pastes into a {@link TextEditor text editor}. * - * In this method, extensions can return a workspace edit that replaces the standard pasting behavior. + * Returned edits can replace the standard pasting behavior. * * @param document Document being pasted into - * @param ranges Currently selected ranges in the document. - * @param dataTransfer The data transfer associated with the paste. + * @param ranges Range in the {@linkcode document} to paste into. + * @param dataTransfer The {@link DataTransfer data transfer} associated with the paste. This object is only + * valid for the duration of the paste operation. + * @param context Additional context for the paste. * @param token A cancellation token. * - * @return Optional workspace edit that applies the paste. Return undefined to use standard pasting. + * @return Set of potential {@link DocumentPasteEdit edits} that can apply the paste. Only a single returned + * {@linkcode DocumentPasteEdit} is applied at a time. If multiple edits are returned from all providers, then + * the first is automatically applied and a widget is shown that lets the user switch to the other edits. */ - provideDocumentPasteEdits?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, token: CancellationToken): ProviderResult; + provideDocumentPasteEdits?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, context: DocumentPasteEditContext, token: CancellationToken): + ProviderResult; + + /** + * Optional method which fills in the {@linkcode DocumentPasteEdit.additionalEdit} before the edit is applied. + * + * This is called once per edit and should be used if generating the complete edit may take a long time. + * Resolve can only be used to change {@linkcode DocumentPasteEdit.additionalEdit}. + * + * @param pasteEdit The {@linkcode DocumentPasteEdit} to resolve. + * @param token A cancellation token. + * + * @returns The resolved paste edit or a thenable that resolves to such. It is OK to return the given + * `pasteEdit`. If no result is returned, the given `pasteEdit` is used. + */ + resolveDocumentPasteEdit?(pasteEdit: T, token: CancellationToken): ProviderResult; } /** - * An operation applied on paste + * An edit the applies a paste operation. */ class DocumentPasteEdit { + /** * Human readable label that describes the edit. */ - label: string; + title: string; /** - * Controls the ordering or multiple paste edits. If this provider yield to edits, it will be shown lower in the list. + * {@link DocumentDropOrPasteEditKind Kind} of the edit. */ - yieldTo?: ReadonlyArray< - | { readonly extensionId: string; readonly providerId: string } - | { readonly mimeType: string } - >; + kind: DocumentDropOrPasteEditKind; /** * The text or snippet to insert at the pasted locations. + * + * If your edit requires more advanced insertion logic, set this to an empty string and provide an {@link DocumentPasteEdit.additionalEdit additional edit} instead. */ insertText: string | SnippetString; @@ -83,30 +180,109 @@ export module '@theia/plugin' { additionalEdit?: WorkspaceEdit; /** - * @param insertText The text or snippet to insert at the pasted locations. + * Controls ordering when multiple paste edits can potentially be applied. * - * TODO: Reverse args, but this will break existing consumers :( + * If this edit yields to another, it will be shown lower in the list of possible paste edits shown to the user. */ - constructor(insertText: string | SnippetString, id: string, label: string); + yieldTo?: readonly DocumentDropOrPasteEditKind[]; + + /** + * Create a new paste edit. + * + * @param insertText The text or snippet to insert at the pasted locations. + * @param title Human readable label that describes the edit. + * @param kind {@link DocumentDropOrPasteEditKind Kind} of the edit. + */ + constructor(insertText: string | SnippetString, title: string, kind: DocumentDropOrPasteEditKind); } + /** + * Provides additional metadata about how a {@linkcode DocumentPasteEditProvider} works. + */ interface DocumentPasteProviderMetadata { /** - * Identifies the provider. - * - * This id is used when users configure the default provider for paste. + * List of {@link DocumentDropOrPasteEditKind kinds} that the provider may return in + * {@linkcode DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits}. * - * This id should be unique within the extension but does not need to be unique across extensions. + * This is used to filter out providers when a specific {@link DocumentDropOrPasteEditKind kind} of edit is requested. */ - readonly id: string; + readonly providedPasteEditKinds: readonly DocumentDropOrPasteEditKind[]; /** - * Mime types that {@link DocumentPasteEditProvider.prepareDocumentPaste provideDocumentPasteEdits} may add on copy. + * Mime types that {@linkcode DocumentPasteEditProvider.prepareDocumentPaste prepareDocumentPaste} may add on copy. */ readonly copyMimeTypes?: readonly string[]; /** - * Mime types that {@link DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits} should be invoked for. + * Mime types that {@linkcode DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits} should be invoked for. + * + * This can either be an exact mime type such as `image/png`, or a wildcard pattern such as `image/*`. + * + * Use `text/uri-list` for resources dropped from the explorer or other tree views in the workbench. + * + * Use `files` to indicate that the provider should be invoked if any {@link DataTransferFile files} are present in the {@linkcode DataTransfer}. + * Note that {@linkcode DataTransferFile} entries are only created when pasting content from outside the editor, such as + * from the operating system. + */ + readonly pasteMimeTypes?: readonly string[]; + } + + /** + * TODO on finalization: + * - Add ctor(insertText: string | SnippetString, title?: string, kind?: DocumentDropOrPasteEditKind); Can't be done as this is an extension to an existing class + */ + + export interface DocumentDropEdit { + /** + * Human readable label that describes the edit. + */ + title?: string; + + /** + * {@link DocumentDropOrPasteEditKind Kind} of the edit. + */ + kind: DocumentDropOrPasteEditKind; + + /** + * Controls the ordering or multiple edits. If this provider yield to edits, it will be shown lower in the list. + */ + yieldTo?: readonly DocumentDropOrPasteEditKind[]; + } + + export interface DocumentDropEditProvider { + // Overload that allows returning multiple edits + // Will be merged in on finalization + provideDocumentDropEdits(document: TextDocument, position: Position, dataTransfer: DataTransfer, token: CancellationToken): + ProviderResult; + + /** + * Optional method which fills in the {@linkcode DocumentDropEdit.additionalEdit} before the edit is applied. + * + * This is called once per edit and should be used if generating the complete edit may take a long time. + * Resolve can only be used to change {@link DocumentDropEdit.additionalEdit}. + * + * @param pasteEdit The {@linkcode DocumentDropEdit} to resolve. + * @param token A cancellation token. + * + * @returns The resolved edit or a thenable that resolves to such. It is OK to return the given + * `edit`. If no result is returned, the given `edit` is used. + */ + resolveDocumentDropEdit?(edit: T, token: CancellationToken): ProviderResult; + } + + /** + * Provides additional metadata about how a {@linkcode DocumentDropEditProvider} works. + */ + export interface DocumentDropEditProviderMetadata { + /** + * List of {@link DocumentDropOrPasteEditKind kinds} that the provider may return in {@linkcode DocumentDropEditProvider.provideDocumentDropEdits provideDocumentDropEdits}. + * + * This is used to filter out providers when a specific {@link DocumentDropOrPasteEditKind kind} of edit is requested. + */ + readonly providedDropEditKinds?: readonly DocumentDropOrPasteEditKind[]; + + /** + * List of {@link DataTransfer} mime types that the provider can handle. * * This can either be an exact mime type such as `image/png`, or a wildcard pattern such as `image/*`. * @@ -116,10 +292,25 @@ export module '@theia/plugin' { * Note that {@link DataTransferFile} entries are only created when dropping content from outside the editor, such as * from the operating system. */ - readonly pasteMimeTypes?: readonly string[]; + readonly dropMimeTypes: readonly string[]; } namespace languages { + /** + * Registers a new {@linkcode DocumentPasteEditProvider}. + * + * @param selector A selector that defines the documents this provider applies to. + * @param provider A paste editor provider. + * @param metadata Additional metadata about the provider. + * + * @returns A {@link Disposable} that unregisters this provider when disposed of. + * @stubbed + */ export function registerDocumentPasteEditProvider(selector: DocumentSelector, provider: DocumentPasteEditProvider, metadata: DocumentPasteProviderMetadata): Disposable; + + /** + * Overload which adds extra metadata. Will be removed on finalization. + */ + export function registerDocumentDropEditProvider(selector: DocumentSelector, provider: DocumentDropEditProvider, metadata?: DocumentDropEditProviderMetadata): Disposable; } } diff --git a/packages/plugin/src/theia.proposed.dropMetadata.d.ts b/packages/plugin/src/theia.proposed.dropMetadata.d.ts deleted file mode 100644 index a8f58d436f2f3..0000000000000 --- a/packages/plugin/src/theia.proposed.dropMetadata.d.ts +++ /dev/null @@ -1,75 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2023 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 -// ***************************************************************************** - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// code copied and modified from https://github.com/microsoft/vscode/blob/1.79.0/src/vscode-dts/vscode.proposed.dropMetadata.d.ts - -export module '@theia/plugin' { - - // https://github.com/microsoft/vscode/issues/179430 - - export interface DocumentDropEdit { - - /** - * Human readable label that describes the edit. - */ - label?: string; - - /** - * The mime type from the {@link DataTransfer} that this edit applies. - */ - handledMimeType?: string; - - /** - * Controls the ordering or multiple paste edits. If this provider yield to edits, it will be shown lower in the list. - */ - yieldTo?: ReadonlyArray< - | { readonly extensionId: string; readonly providerId: string } - | { readonly mimeType: string } - >; - } - - export interface DocumentDropEditProviderMetadata { - /** - * Identifies the provider. - * - * This id is used when users configure the default provider for drop. - * - * This id should be unique within the extension but does not need to be unique across extensions. - */ - readonly id: string; - - /** - * List of data transfer types that the provider supports. - * - * This can either be an exact mime type such as `image/png`, or a wildcard pattern such as `image/*`. - * - * Use `text/uri-list` for resources dropped from the explorer or other tree views in the workbench. - * - * Use `files` to indicate that the provider should be invoked if any {@link DataTransferFile files} are present in the {@link DataTransfer}. - * Note that {@link DataTransferFile} entries are only created when dropping content from outside the editor, such as - * from the operating system. - */ - readonly dropMimeTypes: readonly string[]; - } - - export namespace languages { - export function registerDocumentDropEditProvider(selector: DocumentSelector, provider: DocumentDropEditProvider, metadata?: DocumentDropEditProviderMetadata): Disposable; - } -} diff --git a/packages/plugin/src/theia.proposed.extensionsAny.d.ts b/packages/plugin/src/theia.proposed.extensionsAny.d.ts index 7d05daae7341e..7346670ba1482 100644 --- a/packages/plugin/src/theia.proposed.extensionsAny.d.ts +++ b/packages/plugin/src/theia.proposed.extensionsAny.d.ts @@ -41,9 +41,13 @@ export module '@theia/plugin' { * @param extensionId An extension identifier. * @param includeDifferentExtensionHosts Include extensions from different extension host * @return An extension or `undefined`. + * + * *Note* In Theia, includeDifferentExtensionHosts will always be set to false, as we only support one host currently. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getExtension(extensionId: string, includeDifferentExtensionHosts: boolean): Extension | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export function getExtension(extensionId: string, includeDifferentExtensionHosts: true): Extension | undefined; /** * All extensions across all extension hosts. diff --git a/packages/plugin/src/theia.proposed.mappedEditsProvider.d.ts b/packages/plugin/src/theia.proposed.mappedEditsProvider.d.ts new file mode 100644 index 0000000000000..cedc13e167e6b --- /dev/null +++ b/packages/plugin/src/theia.proposed.mappedEditsProvider.d.ts @@ -0,0 +1,59 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export module '@theia/plugin' { + + export interface DocumentContextItem { + readonly uri: Uri; + readonly version: number; + readonly ranges: Range[]; + } + + export interface MappedEditsContext { + documents: DocumentContextItem[][]; + } + + /** + * Interface for providing mapped edits for a given document. + */ + export interface MappedEditsProvider { + /** + * Provide mapped edits for a given document. + * @param document The document to provide mapped edits for. + * @param codeBlocks Code blocks that come from an LLM's reply. + * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks + * and let the lang server decide what to do with them. + * @param context The context for providing mapped edits. + * @param token A cancellation token. + * @returns A provider result of text edits. + */ + provideMappedEdits( + document: TextDocument, + codeBlocks: string[], + context: MappedEditsContext, + token: CancellationToken + ): ProviderResult; + } + + export namespace chat { + export function registerMappedEditsProvider(documentSelector: DocumentSelector, provider: MappedEditsProvider): Disposable; + } +} diff --git a/packages/plugin/src/theia.proposed.multiDocumentHighlightProvider.ts b/packages/plugin/src/theia.proposed.multiDocumentHighlightProvider.ts new file mode 100644 index 0000000000000..6f3206d522a2b --- /dev/null +++ b/packages/plugin/src/theia.proposed.multiDocumentHighlightProvider.ts @@ -0,0 +1,82 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 +// ***************************************************************************** + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// code copied and modified from https://github.com/microsoft/vscode/blob/1.85.1/src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts + +declare module '@theia/plugin' { + + /** + * Represents a collection of document highlights from multiple documents. + */ + export class MultiDocumentHighlight { + + /** + * The URI of the document containing the highlights. + */ + uri: Uri; + + /** + * The highlights for the document. + */ + highlights: DocumentHighlight[]; + + /** + * Creates a new instance of MultiDocumentHighlight. + * @param uri The URI of the document containing the highlights. + * @param highlights The highlights for the document. + */ + constructor(uri: Uri, highlights: DocumentHighlight[]); + } + + export interface MultiDocumentHighlightProvider { + + /** + * Provide a set of document highlights, like all occurrences of a variable or + * all exit-points of a function. + * + * @param document The document in which the command was invoked. + * @param position The position at which the command was invoked. + * @param otherDocuments An array of additional valid documents for which highlights should be provided. + * @param token A cancellation token. + * @returns A Map containing a mapping of the Uri of a document to the document highlights or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined`, `null`, or an empty map. + */ + provideMultiDocumentHighlights(document: TextDocument, position: Position, otherDocuments: TextDocument[], token: CancellationToken): + ProviderResult; + } + + namespace languages { + + /** + * Register a multi document highlight provider. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their {@link languages.match score} and groups sequentially asked for document highlights. + * The process stops when a provider returns a `non-falsy` or `non-failure` result. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A multi-document highlight provider. + * @returns A {@link Disposable} that unregisters this provider when being disposed. + * @stubbed + */ + export function registerMultiDocumentHighlightProvider(selector: DocumentSelector, provider: MultiDocumentHighlightProvider): Disposable; + } + +} diff --git a/packages/plugin/src/theia.proposed.portsAttributes.d.ts b/packages/plugin/src/theia.proposed.portsAttributes.d.ts new file mode 100644 index 0000000000000..b67d15c900342 --- /dev/null +++ b/packages/plugin/src/theia.proposed.portsAttributes.d.ts @@ -0,0 +1,115 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module '@theia/plugin' { + + /** + * The action that should be taken when a port is discovered through automatic port forwarding discovery. + */ + export enum PortAutoForwardAction { + /** + * Notify the user that the port is being forwarded. This is the default action. + */ + Notify = 1, + /** + * Once the port is forwarded, open the user's web browser to the forwarded port. + */ + OpenBrowser = 2, + /** + * Once the port is forwarded, open the preview browser to the forwarded port. + */ + OpenPreview = 3, + /** + * Forward the port silently. + */ + Silent = 4, + /** + * Do not forward the port. + */ + Ignore = 5 + } + + /** + * The attributes that a forwarded port can have. + */ + export class PortAttributes { + /** + * The action to be taken when this port is detected for auto forwarding. + */ + autoForwardAction: PortAutoForwardAction; + + /** + * Creates a new PortAttributes object + * @param port the port number + * @param autoForwardAction the action to take when this port is detected + */ + constructor(autoForwardAction: PortAutoForwardAction); + } + + /** + * A provider of port attributes. Port attributes are used to determine what action should be taken when a port is discovered. + */ + export interface PortAttributesProvider { + /** + * Provides attributes for the given port. For ports that your extension doesn't know about, simply + * return undefined. For example, if `providePortAttributes` is called with ports 3000 but your + * extension doesn't know anything about 3000 you should return undefined. + * @param port The port number of the port that attributes are being requested for. + * @param pid The pid of the process that is listening on the port. If the pid is unknown, undefined will be passed. + * @param commandLine The command line of the process that is listening on the port. If the command line is unknown, undefined will be passed. + * @param token A cancellation token that indicates the result is no longer needed. + */ + providePortAttributes(attributes: { port: number; pid?: number; commandLine?: string }, token: CancellationToken): ProviderResult; + } + + /** + * A selector that will be used to filter which {@link PortAttributesProvider} should be called for each port. + */ + export interface PortAttributesSelector { + /** + * Specifying a port range will cause your provider to only be called for ports within the range. + * The start is inclusive and the end is exclusive. + */ + portRange?: [number, number] | number; + + /** + * Specifying a command pattern will cause your provider to only be called for processes whose command line matches the pattern. + */ + commandPattern?: RegExp; + } + + export namespace workspace { + /** + * If your extension listens on ports, consider registering a PortAttributesProvider to provide information + * about the ports. For example, a debug extension may know about debug ports in it's debuggee. By providing + * this information with a PortAttributesProvider the extension can tell the editor that these ports should be + * ignored, since they don't need to be user facing. + * + * The results of the PortAttributesProvider are merged with the user setting `remote.portsAttributes`. If the values conflict, the user setting takes precedence. + * + * @param portSelector It is best practice to specify a port selector to avoid unnecessary calls to your provider. + * If you don't specify a port selector your provider will be called for every port, which will result in slower port forwarding for the user. + * @param provider The {@link PortAttributesProvider PortAttributesProvider}. + * @stubbed + */ + export function registerPortAttributesProvider(portSelector: PortAttributesSelector, provider: PortAttributesProvider): Disposable; + } +} diff --git a/packages/plugin/src/theia.proposed.terminalQuickFixProvider.d.ts b/packages/plugin/src/theia.proposed.terminalQuickFixProvider.d.ts index 60d86230880c4..7dcebe7a97459 100644 --- a/packages/plugin/src/theia.proposed.terminalQuickFixProvider.d.ts +++ b/packages/plugin/src/theia.proposed.terminalQuickFixProvider.d.ts @@ -40,7 +40,7 @@ export module '@theia/plugin' { * @return Terminal quick fix(es) if any */ provideTerminalQuickFixes(commandMatchResult: TerminalCommandMatchResult, token: CancellationToken): - ProviderResult>; + ProviderResult>; } export interface TerminalCommandMatchResult { @@ -52,12 +52,16 @@ export module '@theia/plugin' { }; } - export class TerminalQuickFixExecuteTerminalCommand { + export class TerminalQuickFixTerminalCommand { /** - * The terminal command to run + * The terminal command to insert or run */ terminalCommand: string; - constructor(terminalCommand: string); + /** + * Whether the command should be executed or just inserted (default) + */ + shouldExecute?: boolean; + constructor(terminalCommand: string, shouldExecute?: boolean); } export class TerminalQuickFixOpener { /** diff --git a/packages/preferences/package.json b/packages/preferences/package.json index fb541228a2ddd..83f970627b727 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -1,26 +1,28 @@ { "name": "@theia/preferences", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Preferences Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/monaco": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/userstorage": "1.44.0", - "@theia/workspace": "1.44.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/userstorage": "1.54.0", + "@theia/workspace": "1.54.0", "async-mutex": "^0.3.1", "fast-deep-equal": "^3.1.3", "jsonc-parser": "^2.2.0", - "p-debounce": "^2.1.0" + "p-debounce": "^2.1.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" }, "theiaExtensions": [ { - "frontend": "lib/browser/preference-frontend-module" + "frontend": "lib/browser/preference-frontend-module", + "backend": "lib/node/preference-backend-module" } ], "keywords": [ @@ -48,7 +50,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/preferences/src/browser/preference-frontend-contribution.ts b/packages/preferences/src/browser/preference-frontend-contribution.ts new file mode 100644 index 0000000000000..6f8a025236ff5 --- /dev/null +++ b/packages/preferences/src/browser/preference-frontend-contribution.ts @@ -0,0 +1,38 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { CliPreferences } from '../common/cli-preferences'; +import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser/preferences/preference-service'; + +@injectable() +export class PreferenceFrontendContribution implements FrontendApplicationContribution { + @inject(CliPreferences) + protected readonly CliPreferences: CliPreferences; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + onStart(): void { + this.CliPreferences.getPreferences().then(async preferences => { + await this.preferenceService.ready; + for (const [key, value] of preferences) { + this.preferenceService.set(key, value, PreferenceScope.User); + } + }); + } +} diff --git a/packages/preferences/src/browser/preference-frontend-module.ts b/packages/preferences/src/browser/preference-frontend-module.ts index ec3c01aace7b4..d55f47e63d60b 100644 --- a/packages/preferences/src/browser/preference-frontend-module.ts +++ b/packages/preferences/src/browser/preference-frontend-module.ts @@ -17,7 +17,7 @@ import '../../src/browser/style/index.css'; import './preferences-monaco-contribution'; import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; -import { bindViewContribution, OpenHandler } from '@theia/core/lib/browser'; +import { bindViewContribution, FrontendApplicationContribution, noopWidgetStatusBarContribution, OpenHandler, WidgetStatusBarContribution } from '@theia/core/lib/browser'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { PreferenceTreeGenerator } from './util/preference-tree-generator'; import { bindPreferenceProviders } from './preference-bindings'; @@ -29,12 +29,18 @@ import { PreferencesJsonSchemaContribution } from './preferences-json-schema-con import { MonacoJSONCEditor } from './monaco-jsonc-editor'; import { PreferenceTransaction, PreferenceTransactionFactory, preferenceTransactionFactoryCreator } from './preference-transaction-manager'; import { PreferenceOpenHandler } from './preference-open-handler'; +import { CliPreferences, CliPreferencesPath } from '../common/cli-preferences'; +import { ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider'; +import { PreferenceFrontendContribution } from './preference-frontend-contribution'; +import { PreferenceLayoutProvider } from './util/preference-layout'; +import { PreferencesWidget } from './views/preference-widget'; export function bindPreferences(bind: interfaces.Bind, unbind: interfaces.Unbind): void { bindPreferenceProviders(bind, unbind); bindPreferencesWidgets(bind); bind(PreferenceTreeGenerator).toSelf().inSingletonScope(); + bind(PreferenceLayoutProvider).toSelf().inSingletonScope(); bindViewContribution(bind, PreferencesContribution); @@ -50,6 +56,12 @@ export function bindPreferences(bind: interfaces.Bind, unbind: interfaces.Unbind bind(MonacoJSONCEditor).toSelf().inSingletonScope(); bind(PreferenceTransaction).toSelf(); bind(PreferenceTransactionFactory).toFactory(preferenceTransactionFactoryCreator); + + bind(CliPreferences).toDynamicValue(ctx => ServiceConnectionProvider.createProxy(ctx.container, CliPreferencesPath)).inSingletonScope(); + bind(PreferenceFrontendContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(PreferenceFrontendContribution); + + bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(PreferencesWidget)); } export default new ContainerModule((bind, unbind, isBound, rebind) => { diff --git a/packages/preferences/src/browser/preference-tree-model.ts b/packages/preferences/src/browser/preference-tree-model.ts index c03ecf19afb96..a0d3833669339 100644 --- a/packages/preferences/src/browser/preference-tree-model.ts +++ b/packages/preferences/src/browser/preference-tree-model.ts @@ -30,11 +30,12 @@ import { } from '@theia/core/lib/browser'; import { Emitter } from '@theia/core'; import { PreferencesSearchbarWidget } from './views/preference-searchbar-widget'; -import { PreferenceTreeGenerator, COMMONLY_USED_SECTION_PREFIX } from './util/preference-tree-generator'; +import { PreferenceTreeGenerator } from './util/preference-tree-generator'; import * as fuzzy from '@theia/core/shared/fuzzy'; import { PreferencesScopeTabBar } from './views/preference-scope-tabbar-widget'; import { Preference } from './util/preference-types'; import { Event } from '@theia/core/lib/common'; +import { COMMONLY_USED_SECTION_PREFIX } from './util/preference-layout'; export interface PreferenceTreeNodeProps extends NodeProps { visibleChildren: number; @@ -67,6 +68,7 @@ export class PreferenceTreeModel extends TreeModelImpl { protected lastSearchedFuzzy: string = ''; protected lastSearchedLiteral: string = ''; + protected lastSearchedTags: string[] = []; protected _currentScope: number = Number(Preference.DEFAULT_SCOPE.scope); protected _isFiltered: boolean = false; protected _currentRows: Map = new Map(); @@ -110,8 +112,10 @@ export class PreferenceTreeModel extends TreeModelImpl { this.updateFilteredRows(PreferenceFilterChangeSource.Scope); }), this.filterInput.onFilterChanged(newSearchTerm => { - this.lastSearchedLiteral = newSearchTerm; - this.lastSearchedFuzzy = newSearchTerm.replace(/\s/g, ''); + this.lastSearchedTags = Array.from(newSearchTerm.matchAll(/@tag:([^\s]+)/g)).map(match => match[0].slice(5)); + const newSearchTermWithoutTags = newSearchTerm.replace(/@tag:[^\s]+/g, ''); + this.lastSearchedLiteral = newSearchTermWithoutTags; + this.lastSearchedFuzzy = newSearchTermWithoutTags.replace(/\s/g, ''); this._isFiltered = newSearchTerm.length > 2; if (this.isFiltered) { this.expandAll(); @@ -183,6 +187,9 @@ export class PreferenceTreeModel extends TreeModelImpl { if (node.id.startsWith(COMMONLY_USED_SECTION_PREFIX)) { return false; } + if (!this.lastSearchedTags.every(tag => node.preference.data.tags?.includes(tag))) { + return false; + } return fuzzy.test(this.lastSearchedFuzzy, prefID) // search matches preference name. // search matches description. Fuzzy isn't ideal here because the score depends on the order of discovery. || (node.preference.data.description ?? '').includes(this.lastSearchedLiteral); @@ -209,12 +216,15 @@ export class PreferenceTreeModel extends TreeModelImpl { } collapseAllExcept(openNode: TreeNode | undefined): void { - if (ExpandableTreeNode.is(openNode)) { + const openNodes: TreeNode[] = []; + while (ExpandableTreeNode.is(openNode)) { + openNodes.push(openNode); this.expandNode(openNode); + openNode = openNode.parent; } if (CompositeTreeNode.is(this.root)) { this.root.children.forEach(child => { - if (child !== openNode && ExpandableTreeNode.is(child)) { + if (!openNodes.includes(child) && ExpandableTreeNode.is(child)) { this.collapseNode(child); } }); diff --git a/packages/preferences/src/browser/util/preference-layout.ts b/packages/preferences/src/browser/util/preference-layout.ts new file mode 100644 index 0000000000000..d810e3e0340b1 --- /dev/null +++ b/packages/preferences/src/browser/util/preference-layout.ts @@ -0,0 +1,381 @@ +// ***************************************************************************** +// 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 { nls } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; + +export interface PreferenceLayout { + id: string; + label: string; + children?: PreferenceLayout[]; + settings?: string[]; +} + +export const COMMONLY_USED_SECTION_PREFIX = 'commonly-used'; + +export const COMMONLY_USED_LAYOUT = { + id: COMMONLY_USED_SECTION_PREFIX, + label: nls.localizeByDefault('Commonly Used'), + settings: [ + 'files.autoSave', + 'editor.fontSize', + 'editor.fontFamily', + 'editor.tabSize', + 'editor.renderWhitespace', + 'editor.cursorStyle', + 'editor.multiCursorModifier', + 'editor.insertSpaces', + 'editor.wordWrap', + 'files.exclude', + 'files.associations' + ] +}; + +export const DEFAULT_LAYOUT: PreferenceLayout[] = [ + { + id: 'editor', + label: nls.localizeByDefault('Text Editor'), + settings: ['editor.*'], + children: [ + { + id: 'editor.cursor', + label: nls.localizeByDefault('Cursor'), + settings: ['editor.cursor*'] + }, + { + id: 'editor.find', + label: nls.localizeByDefault('Find'), + settings: ['editor.find.*'] + }, + { + id: 'editor.font', + label: nls.localizeByDefault('Font'), + settings: ['editor.font*'] + }, + { + id: 'editor.format', + label: nls.localizeByDefault('Formatting'), + settings: ['editor.format*'] + }, + { + id: 'editor.diffEditor', + label: nls.localizeByDefault('Diff Editor'), + settings: ['diffEditor.*'] + }, + { + id: 'editor.multiDiffEditor', + label: nls.localizeByDefault('Multi-File Diff Editor'), + settings: ['multiDiffEditor.*'] + }, + { + id: 'editor.minimap', + label: nls.localizeByDefault('Minimap'), + settings: ['editor.minimap.*'] + }, + { + id: 'editor.suggestions', + label: nls.localizeByDefault('Suggestions'), + settings: ['editor.*suggest*'] + }, + { + id: 'editor.files', + label: nls.localizeByDefault('Files'), + settings: ['files.*'] + } + ] + }, + { + id: 'workbench', + label: nls.localizeByDefault('Workbench'), + settings: ['workbench.*', 'workspace.*'], + children: [ + { + id: 'workbench.appearance', + label: nls.localizeByDefault('Appearance'), + settings: [ + 'workbench.activityBar.*', 'workbench.*color*', 'workbench.fontAliasing', 'workbench.iconTheme', 'workbench.sidebar.location', + 'workbench.*.visible', 'workbench.tips.enabled', 'workbench.tree.*', 'workbench.view.*' + ] + }, + { + id: 'workbench.breadcrumbs', + label: nls.localizeByDefault('Breadcrumbs'), + settings: ['breadcrumbs.*'] + }, + { + id: 'workbench.editor', + label: nls.localizeByDefault('Editor Management'), + settings: ['workbench.editor.*'] + }, + { + id: 'workbench.settings', + label: nls.localizeByDefault('Settings Editor'), + settings: ['workbench.settings.*'] + }, + { + id: 'workbench.zenmode', + label: nls.localizeByDefault('Zen Mode'), + settings: ['zenmode.*'] + }, + { + id: 'workbench.screencastmode', + label: nls.localizeByDefault('Screencast Mode'), + settings: ['screencastMode.*'] + } + ] + }, + { + id: 'window', + label: nls.localizeByDefault('Window'), + settings: ['window.*'], + children: [ + { + id: 'window.newWindow', + label: nls.localizeByDefault('New Window'), + settings: ['window.*newwindow*'] + } + ] + }, + { + id: 'features', + label: nls.localizeByDefault('Features'), + children: [ + { + id: 'features.accessibilitySignals', + label: nls.localizeByDefault('Accessibility Signals'), + settings: ['accessibility.signal*'] + }, + { + id: 'features.accessibility', + label: nls.localizeByDefault('Accessibility'), + settings: ['accessibility.*'] + }, + { + id: 'features.explorer', + label: nls.localizeByDefault('Explorer'), + settings: ['explorer.*', 'outline.*'] + }, + { + id: 'features.search', + label: nls.localizeByDefault('Search'), + settings: ['search.*'] + }, + { + id: 'features.debug', + label: nls.localizeByDefault('Debug'), + settings: ['debug.*', 'launch'] + }, + { + id: 'features.testing', + label: nls.localizeByDefault('Testing'), + settings: ['testing.*'] + }, + { + id: 'features.scm', + label: nls.localizeByDefault('Source Control'), + settings: ['scm.*'] + }, + { + id: 'features.extensions', + label: nls.localizeByDefault('Extensions'), + settings: ['extensions.*'] + }, + { + id: 'features.terminal', + label: nls.localizeByDefault('Terminal'), + settings: ['terminal.*'] + }, + { + id: 'features.task', + label: nls.localizeByDefault('Task'), + settings: ['task.*'] + }, + { + id: 'features.problems', + label: nls.localizeByDefault('Problems'), + settings: ['problems.*'] + }, + { + id: 'features.output', + label: nls.localizeByDefault('Output'), + settings: ['output.*'] + }, + { + id: 'features.comments', + label: nls.localizeByDefault('Comments'), + settings: ['comments.*'] + }, + { + id: 'features.remote', + label: nls.localizeByDefault('Remote'), + settings: ['remote.*'] + }, + { + id: 'features.timeline', + label: nls.localizeByDefault('Timeline'), + settings: ['timeline.*'] + }, + { + id: 'features.toolbar', + label: nls.localize('theia/preferences/toolbar', 'Toolbar'), + settings: ['toolbar.*'] + }, + { + id: 'features.notebook', + label: nls.localizeByDefault('Notebook'), + settings: ['notebook.*', 'interactiveWindow.*'] + }, + { + id: 'features.mergeEditor', + label: nls.localizeByDefault('Merge Editor'), + settings: ['mergeEditor.*'] + }, + { + id: 'features.chat', + label: nls.localizeByDefault('Chat'), + settings: ['chat.*', 'inlineChat.*'] + } + ] + }, + { + id: 'application', + label: nls.localizeByDefault('Application'), + children: [ + { + id: 'application.http', + label: nls.localizeByDefault('HTTP'), + settings: ['http.*'] + }, + { + id: 'application.keyboard', + label: nls.localizeByDefault('Keyboard'), + settings: ['keyboard.*'] + }, + { + id: 'application.update', + label: nls.localizeByDefault('Update'), + settings: ['update.*'] + }, + { + id: 'application.telemetry', + label: nls.localizeByDefault('Telemetry'), + settings: ['telemetry.*'] + }, + { + id: 'application.settingsSync', + label: nls.localizeByDefault('Settings Sync'), + settings: ['settingsSync.*'] + }, + { + id: 'application.experimental', + label: nls.localizeByDefault('Experimental'), + settings: ['application.experimental.*'] + }, + { + id: 'application.other', + label: nls.localizeByDefault('Other'), + settings: ['application.*'] + } + ] + }, + { + id: 'security', + label: nls.localizeByDefault('Security'), + settings: ['security.*'], + children: [ + { + id: 'security.workspace', + label: nls.localizeByDefault('Workspace'), + settings: ['security.workspace.*'] + } + ] + }, + { + id: 'ai-features', + label: 'AI Features', // TODO localize + }, + { + id: 'extensions', + label: nls.localizeByDefault('Extensions'), + children: [ + { + id: 'extensions.hosted-plugin', + label: nls.localize('theia/preferences/hostedPlugin', 'Hosted Plugin'), + settings: ['hosted-plugin.*'] + } + ] + } +]; + +@injectable() +export class PreferenceLayoutProvider { + + getLayout(): PreferenceLayout[] { + return DEFAULT_LAYOUT; + } + + getCommonlyUsedLayout(): PreferenceLayout { + return COMMONLY_USED_LAYOUT; + } + + hasCategory(id: string): boolean { + return [...this.getLayout(), this.getCommonlyUsedLayout()].some(e => e.id === id); + } + + getLayoutForPreference(preferenceId: string): PreferenceLayout | undefined { + const layout = this.getLayout(); + for (const section of layout) { + const item = this.findItemInSection(section, preferenceId); + if (item) { + return item; + } + } + return undefined; + } + + protected findItemInSection(section: PreferenceLayout, preferenceId: string): PreferenceLayout | undefined { + // First check whether any of its children match the preferenceId. + if (section.children) { + for (const child of section.children) { + const item = this.findItemInSection(child, preferenceId); + if (item) { + return item; + } + } + } + // Then check whether the section itself matches the preferenceId. + if (section.settings) { + for (const setting of section.settings) { + if (this.matchesSetting(preferenceId, setting)) { + return section; + } + } + } + return undefined; + } + + protected matchesSetting(preferenceId: string, setting: string): boolean { + if (setting.includes('*')) { + return this.createRegExp(setting).test(preferenceId); + } + return preferenceId === setting; + } + + protected createRegExp(setting: string): RegExp { + return new RegExp(`^${setting.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`); + } + +} diff --git a/packages/preferences/src/browser/util/preference-tree-generator.ts b/packages/preferences/src/browser/util/preference-tree-generator.ts index e8e1bb6d09c09..bacd4035d8534 100644 --- a/packages/preferences/src/browser/util/preference-tree-generator.ts +++ b/packages/preferences/src/browser/util/preference-tree-generator.ts @@ -20,57 +20,28 @@ import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/pr import { Emitter } from '@theia/core'; import debounce = require('@theia/core/shared/lodash.debounce'); import { Preference } from './preference-types'; +import { COMMONLY_USED_SECTION_PREFIX, PreferenceLayoutProvider } from './preference-layout'; + +export interface CreatePreferencesGroupOptions { + id: string, + group: string, + root: CompositeTreeNode, + expanded?: boolean, + depth?: number, + label?: string +} -export const COMMONLY_USED_SECTION_PREFIX = 'commonly-used'; @injectable() export class PreferenceTreeGenerator { @inject(PreferenceSchemaProvider) protected readonly schemaProvider: PreferenceSchemaProvider; @inject(PreferenceConfigurations) protected readonly preferenceConfigs: PreferenceConfigurations; + @inject(PreferenceLayoutProvider) protected readonly layoutProvider: PreferenceLayoutProvider; protected _root: CompositeTreeNode; protected readonly onSchemaChangedEmitter = new Emitter(); readonly onSchemaChanged = this.onSchemaChangedEmitter.event; - protected readonly commonlyUsedPreferences = [ - 'files.autoSave', 'files.autoSaveDelay', 'editor.fontSize', - 'editor.fontFamily', 'editor.tabSize', 'editor.renderWhitespace', - 'editor.cursorStyle', 'editor.multiCursorModifier', 'editor.insertSpaces', - 'editor.wordWrap', 'files.exclude', 'files.associations' - ]; - protected readonly topLevelCategories = new Map([ - [COMMONLY_USED_SECTION_PREFIX, 'Commonly Used'], - ['editor', 'Text Editor'], - ['workbench', 'Workbench'], - ['window', 'Window'], - ['features', 'Features'], - ['application', 'Application'], - ['security', 'Security'], - ['extensions', 'Extensions'] - ]); - protected readonly sectionAssignments = new Map([ - ['breadcrumbs', 'workbench'], - ['comments', 'features'], - ['debug', 'features'], - ['diffEditor', 'editor'], - ['explorer', 'features'], - ['extensions', 'features'], - ['files', 'editor'], - ['hosted-plugin', 'features'], - ['http', 'application'], - ['keyboard', 'application'], - ['notification', 'workbench'], - ['output', 'features'], - ['preview', 'features'], - ['problems', 'features'], - ['scm', 'features'], - ['search', 'features'], - ['task', 'features'], - ['terminal', 'features'], - ['toolbar', 'features'], - ['webview', 'features'], - ['workspace', 'application'], - ]); protected readonly defaultTopLevelCategory = 'extensions'; get root(): CompositeTreeNode { @@ -94,11 +65,25 @@ export class PreferenceTreeGenerator { const groups = new Map(); const root = this.createRootNode(); - for (const id of this.topLevelCategories.keys()) { - this.getOrCreatePreferencesGroup(id, id, root, groups); + const commonlyUsedLayout = this.layoutProvider.getCommonlyUsedLayout(); + const commonlyUsed = this.getOrCreatePreferencesGroup({ + id: commonlyUsedLayout.id, + group: commonlyUsedLayout.id, + root, + groups, + label: commonlyUsedLayout.label + }); + + for (const layout of this.layoutProvider.getLayout()) { + this.getOrCreatePreferencesGroup({ + id: layout.id, + group: layout.id, + root, + groups, + label: layout.label + }); } - const commonlyUsed = this.getOrCreatePreferencesGroup(COMMONLY_USED_SECTION_PREFIX, COMMONLY_USED_SECTION_PREFIX, root, groups); - for (const preference of this.commonlyUsedPreferences) { + for (const preference of commonlyUsedLayout.settings ?? []) { if (preference in preferencesSchema.properties) { this.createLeafNode(preference, commonlyUsed, preferencesSchema.properties[preference]); } @@ -106,13 +91,11 @@ export class PreferenceTreeGenerator { for (const propertyName of propertyNames) { const property = preferencesSchema.properties[propertyName]; if (!this.preferenceConfigs.isSectionName(propertyName) && !OVERRIDE_PROPERTY_PATTERN.test(propertyName) && !property.deprecationMessage) { - const labels = propertyName.split('.'); - const groupID = this.getGroupName(labels); - const subgroupName = this.getSubgroupName(labels, groupID); - const subgroupID = [groupID, subgroupName].join('.'); - const toplevelParent = this.getOrCreatePreferencesGroup(groupID, groupID, root, groups); - const immediateParent = subgroupName && this.getOrCreatePreferencesGroup(subgroupID, groupID, toplevelParent, groups); - this.createLeafNode(propertyName, immediateParent || toplevelParent, property); + if (property.owner) { + this.createPluginLeafNode(propertyName, property, root, groups); + } else { + this.createBuiltinLeafNode(propertyName, property, root, groups); + } } } @@ -136,6 +119,63 @@ export class PreferenceTreeGenerator { return root; }; + protected createBuiltinLeafNode(name: string, property: PreferenceDataProperty, root: CompositeTreeNode, groups: Map): void { + const layoutItem = this.layoutProvider.getLayoutForPreference(name); + const labels = layoutItem ? layoutItem.id.split('.') : name.split('.'); + const groupID = this.getGroupName(labels); + const subgroupName = this.getSubgroupName(labels, groupID); + const subgroupID = [groupID, subgroupName].join('.'); + const toplevelParent = this.getOrCreatePreferencesGroup({ + id: groupID, + group: groupID, + root, + groups + }); + const immediateParent = subgroupName ? this.getOrCreatePreferencesGroup({ + id: subgroupID, + group: groupID, + root: toplevelParent, + groups, + label: layoutItem?.label + }) : undefined; + this.createLeafNode(name, immediateParent || toplevelParent, property); + } + + protected createPluginLeafNode(name: string, property: PreferenceDataProperty, root: CompositeTreeNode, groups: Map): void { + if (!property.owner) { + return; + } + const groupID = this.defaultTopLevelCategory; + const subgroupName = property.owner; + const subsubgroupName = property.group; + const hasGroup = Boolean(subsubgroupName); + const toplevelParent = this.getOrCreatePreferencesGroup({ + id: groupID, + group: groupID, + root, + groups + }); + const subgroupID = [groupID, subgroupName].join('.'); + const subgroupParent = this.getOrCreatePreferencesGroup({ + id: subgroupID, + group: groupID, + root: toplevelParent, + groups, + expanded: hasGroup, + label: subgroupName + }); + const subsubgroupID = [groupID, subgroupName, subsubgroupName].join('.'); + const subsubgroupParent = hasGroup ? this.getOrCreatePreferencesGroup({ + id: subsubgroupID, + group: subgroupID, + root: subgroupParent, + groups, + depth: 2, + label: subsubgroupName + }) : undefined; + this.createLeafNode(name, subsubgroupParent || subgroupParent, property); + } + getNodeId(preferenceId: string): string { const expectedGroup = this.getGroupName(preferenceId.split('.')); const expectedId = `${expectedGroup}@${preferenceId}`; @@ -144,21 +184,19 @@ export class PreferenceTreeGenerator { protected getGroupName(labels: string[]): string { const defaultGroup = labels[0]; - if (this.topLevelCategories.has(defaultGroup)) { + if (this.layoutProvider.hasCategory(defaultGroup)) { return defaultGroup; } - const assignedGroup = this.sectionAssignments.get(defaultGroup); - if (assignedGroup) { - return assignedGroup; - } return this.defaultTopLevelCategory; } protected getSubgroupName(labels: string[], computedGroupName: string): string | undefined { if (computedGroupName !== labels[0]) { return labels[0]; - } else if (labels.length > 2) { + } else if (labels.length > 1) { return labels[1]; + } else { + return undefined; } } @@ -181,46 +219,42 @@ export class PreferenceTreeGenerator { protected createLeafNode(property: string, preferencesGroup: Preference.CompositeTreeNode, data: PreferenceDataProperty): Preference.LeafNode { const { group } = Preference.TreeNode.getGroupAndIdFromNodeId(preferencesGroup.id); - const newNode = { + const newNode: Preference.LeafNode = { id: `${group}@${property}`, preferenceId: property, parent: preferencesGroup, preference: { data }, - depth: Preference.TreeNode.isTopLevel(preferencesGroup) ? 1 : 2, + depth: Preference.TreeNode.isTopLevel(preferencesGroup) ? 1 : 2 }; CompositeTreeNode.addChild(preferencesGroup, newNode); return newNode; } - protected createPreferencesGroup(id: string, group: string, root: CompositeTreeNode): Preference.CompositeTreeNode { - const newNode = { - id: `${group}@${id}`, + protected createPreferencesGroup(options: CreatePreferencesGroupOptions): Preference.CompositeTreeNode { + const newNode: Preference.CompositeTreeNode = { + id: `${options.group}@${options.id}`, visible: true, - parent: root, + parent: options.root, children: [], expanded: false, selected: false, depth: 0, + label: options.label }; const isTopLevel = Preference.TreeNode.isTopLevel(newNode); - if (!isTopLevel) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (newNode as any).expanded; + if (!(options.expanded ?? isTopLevel)) { + delete newNode.expanded; } - newNode.depth = isTopLevel ? 0 : 1; - CompositeTreeNode.addChild(root, newNode); + newNode.depth = options.depth ?? (isTopLevel ? 0 : 1); + CompositeTreeNode.addChild(options.root, newNode); return newNode; } - getCustomLabelFor(id: string): string | undefined { - return this.topLevelCategories.get(id); - } - - protected getOrCreatePreferencesGroup(id: string, group: string, root: CompositeTreeNode, groups: Map): Preference.CompositeTreeNode { - const existingGroup = groups.get(id); + protected getOrCreatePreferencesGroup(options: CreatePreferencesGroupOptions & { groups: Map }): Preference.CompositeTreeNode { + const existingGroup = options.groups.get(options.id); if (existingGroup) { return existingGroup; } - const newNode = this.createPreferencesGroup(id, group, root); - groups.set(id, newNode); + const newNode = this.createPreferencesGroup(options); + options.groups.set(options.id, newNode); return newNode; }; } diff --git a/packages/preferences/src/browser/util/preference-tree-label-provider.spec.ts b/packages/preferences/src/browser/util/preference-tree-label-provider.spec.ts index 06b764580b449..d50b22baef6c3 100644 --- a/packages/preferences/src/browser/util/preference-tree-label-provider.spec.ts +++ b/packages/preferences/src/browser/util/preference-tree-label-provider.spec.ts @@ -28,6 +28,7 @@ import { PreferenceTreeGenerator } from './preference-tree-generator'; import { PreferenceTreeLabelProvider } from './preference-tree-label-provider'; import { Preference } from './preference-types'; import { SelectableTreeNode } from '@theia/core/lib/browser'; +import { PreferenceLayoutProvider } from './preference-layout'; disableJSDOM(); @@ -37,6 +38,7 @@ describe('preference-tree-label-provider', () => { beforeEach(() => { const container = new Container(); + container.bind(PreferenceLayoutProvider).toSelf().inSingletonScope(); container.bind(PreferenceTreeGenerator).toConstantValue({ getCustomLabelFor: () => { } }); preferenceTreeLabelProvider = container.resolve(PreferenceTreeLabelProvider); }); diff --git a/packages/preferences/src/browser/util/preference-tree-label-provider.ts b/packages/preferences/src/browser/util/preference-tree-label-provider.ts index 671bed7be7565..7ed9b0d35f23a 100644 --- a/packages/preferences/src/browser/util/preference-tree-label-provider.ts +++ b/packages/preferences/src/browser/util/preference-tree-label-provider.ts @@ -14,21 +14,29 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject } from '@theia/core/shared/inversify'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { LabelProviderContribution, TreeNode } from '@theia/core/lib/browser'; import { Preference } from './preference-types'; -import { PreferenceTreeGenerator } from './preference-tree-generator'; +import { PreferenceLayoutProvider } from './preference-layout'; + @injectable() export class PreferenceTreeLabelProvider implements LabelProviderContribution { - @inject(PreferenceTreeGenerator) protected readonly treeGenerator: PreferenceTreeGenerator; + + @inject(PreferenceLayoutProvider) + protected readonly layoutProvider: PreferenceLayoutProvider; canHandle(element: object): number { return TreeNode.is(element) && Preference.TreeNode.is(element) ? 150 : 0; } getName(node: Preference.TreeNode): string { + if (Preference.TreeNode.is(node) && node.label) { + return node.label; + } const { id } = Preference.TreeNode.getGroupAndIdFromNodeId(node.id); - return this.formatString(this.treeGenerator.getCustomLabelFor(id) ?? id.split('.').pop()!); + const labels = id.split('.'); + const groupName = labels[labels.length - 1]; + return this.formatString(groupName); } getPrefix(node: Preference.TreeNode, fullPath = false): string | undefined { diff --git a/packages/preferences/src/browser/util/preference-types.ts b/packages/preferences/src/browser/util/preference-types.ts index c901c87a3ae85..bf4a81521548c 100644 --- a/packages/preferences/src/browser/util/preference-types.ts +++ b/packages/preferences/src/browser/util/preference-types.ts @@ -19,6 +19,7 @@ import { PreferenceScope, TreeNode as BaseTreeNode, CompositeTreeNode as BaseCompositeTreeNode, + SelectableTreeNode, PreferenceInspection, CommonCommands, } from '@theia/core/lib/browser'; @@ -58,11 +59,18 @@ export namespace Preference { }; } - export interface CompositeTreeNode extends BaseCompositeTreeNode { + export interface CompositeTreeNode extends BaseCompositeTreeNode, SelectableTreeNode { + expanded?: boolean; depth: number; + label?: string; + } + + export namespace CompositeTreeNode { + export const is = (node: TreeNode): node is CompositeTreeNode => !LeafNode.is(node); } export interface LeafNode extends BaseTreeNode { + label?: string; depth: number; preference: { data: PreferenceDataProperty }; preferenceId: string; diff --git a/packages/preferences/src/browser/views/preference-editor-widget.ts b/packages/preferences/src/browser/views/preference-editor-widget.ts index 4d2e06ea70568..d8405759ad312 100644 --- a/packages/preferences/src/browser/views/preference-editor-widget.ts +++ b/packages/preferences/src/browser/views/preference-editor-widget.ts @@ -33,9 +33,9 @@ import { BaseWidget, DEFAULT_SCROLL_OPTIONS } from '@theia/core/lib/browser/widg import { PreferenceTreeModel, PreferenceFilterChangeEvent, PreferenceFilterChangeSource } from '../preference-tree-model'; import { PreferenceNodeRendererFactory, GeneralPreferenceNodeRenderer } from './components/preference-node-renderer'; import { Preference } from '../util/preference-types'; -import { COMMONLY_USED_SECTION_PREFIX } from '../util/preference-tree-generator'; import { PreferencesScopeTabBar } from './preference-scope-tabbar-widget'; import { PreferenceNodeRendererCreatorRegistry } from './components/preference-node-renderer-creator'; +import { COMMONLY_USED_SECTION_PREFIX } from '../util/preference-layout'; export interface PreferencesEditorState { firstVisibleChildID: string, diff --git a/packages/preferences/src/browser/views/preference-tree-widget.tsx b/packages/preferences/src/browser/views/preference-tree-widget.tsx index 29a94d1875b90..60f51b341c0cd 100644 --- a/packages/preferences/src/browser/views/preference-tree-widget.tsx +++ b/packages/preferences/src/browser/views/preference-tree-widget.tsx @@ -24,6 +24,7 @@ import { } from '@theia/core/lib/browser'; import React = require('@theia/core/shared/react'); import { PreferenceTreeModel, PreferenceTreeNodeRow, PreferenceTreeNodeProps } from '../preference-tree-model'; +import { Preference } from '../util/preference-types'; @injectable() export class PreferencesTreeWidget extends TreeWidget { @@ -50,13 +51,21 @@ export class PreferencesTreeWidget extends TreeWidget { this.rows = new Map(); let index = 0; for (const [id, nodeRow] of this.model.currentRows.entries()) { - if (nodeRow.visibleChildren > 0 && (ExpandableTreeNode.is(nodeRow.node) || ExpandableTreeNode.isExpanded(nodeRow.node.parent))) { + if (nodeRow.visibleChildren > 0 && this.isVisibleNode(nodeRow.node)) { this.rows.set(id, { ...nodeRow, index: index++ }); } } this.updateScrollToRow(); } + protected isVisibleNode(node: Preference.TreeNode): boolean { + if (Preference.TreeNode.isTopLevel(node)) { + return true; + } else { + return ExpandableTreeNode.isExpanded(node.parent) && Preference.TreeNode.is(node.parent) && this.isVisibleNode(node.parent); + } + } + protected override doRenderNodeRow({ depth, visibleChildren, node, isExpansible }: PreferenceTreeNodeRow): React.ReactNode { return this.renderNode(node, { depth, visibleChildren, isExpansible }); } diff --git a/packages/preferences/src/browser/views/preference-widget.tsx b/packages/preferences/src/browser/views/preference-widget.tsx index 709e29a2a9fcf..631bdf478217e 100644 --- a/packages/preferences/src/browser/views/preference-widget.tsx +++ b/packages/preferences/src/browser/views/preference-widget.tsx @@ -78,6 +78,7 @@ export class PreferencesWidget extends Panel implements StatefulWidget { protected init(): void { this.id = PreferencesWidget.ID; this.title.label = PreferencesWidget.LABEL; + this.title.caption = PreferencesWidget.LABEL; this.title.closable = true; this.addClass('theia-settings-container'); this.title.iconClass = codicon('settings'); diff --git a/packages/preferences/src/common/cli-preferences.ts b/packages/preferences/src/common/cli-preferences.ts new file mode 100644 index 0000000000000..f17d5d762dad0 --- /dev/null +++ b/packages/preferences/src/common/cli-preferences.ts @@ -0,0 +1,22 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export const CliPreferences = Symbol('CliPreferences'); +export const CliPreferencesPath = '/services/cli-preferences'; + +export interface CliPreferences { + getPreferences(): Promise<[string, unknown][]>; +} diff --git a/packages/preferences/src/node/preference-backend-module.ts b/packages/preferences/src/node/preference-backend-module.ts new file mode 100644 index 0000000000000..7b0db89f887d0 --- /dev/null +++ b/packages/preferences/src/node/preference-backend-module.ts @@ -0,0 +1,33 @@ +// ***************************************************************************** +// 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { CliContribution } from '@theia/core/lib/node/cli'; +import { PreferenceCliContribution } from './preference-cli-contribution'; +import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module'; +import { CliPreferences, CliPreferencesPath } from '../common/cli-preferences'; + +const preferencesConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { + bindBackendService(CliPreferencesPath, CliPreferences); +}); + +export default new ContainerModule(bind => { + bind(PreferenceCliContribution).toSelf().inSingletonScope(); + bind(CliPreferences).toService(PreferenceCliContribution); + bind(CliContribution).toService(PreferenceCliContribution); + + bind(ConnectionContainerModule).toConstantValue(preferencesConnectionModule); +}); diff --git a/packages/preferences/src/node/preference-cli-contribution.ts b/packages/preferences/src/node/preference-cli-contribution.ts new file mode 100644 index 0000000000000..0fd46963b486e --- /dev/null +++ b/packages/preferences/src/node/preference-cli-contribution.ts @@ -0,0 +1,48 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; +import { Argv } from '@theia/core/shared/yargs'; +import { CliContribution } from '@theia/core/lib/node/cli'; +import { CliPreferences } from '../common/cli-preferences'; + +@injectable() +export class PreferenceCliContribution implements CliContribution, CliPreferences { + + protected preferences: [string, unknown][] = []; + + configure(conf: Argv<{}>): void { + conf.option('set-preference', { + nargs: 1, + desc: 'sets the specified preference' + }); + } + + setArguments(args: Record): void { + if (args.setPreference) { + const preferences: string[] = args.setPreference instanceof Array ? args.setPreference : [args.setPreference]; + for (const preference of preferences) { + const firstEqualIndex = preference.indexOf('='); + this.preferences.push([preference.substring(0, firstEqualIndex), JSON.parse(preference.substring(firstEqualIndex + 1))]); + } + } + } + + async getPreferences(): Promise<[string, unknown][]> { + return this.preferences; + } + +} diff --git a/packages/preview/package.json b/packages/preview/package.json index cb73d607f55ba..0fae6f0b68c67 100644 --- a/packages/preview/package.json +++ b/packages/preview/package.json @@ -1,16 +1,17 @@ { "name": "@theia/preview", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Preview Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/mini-browser": "1.44.0", - "@theia/monaco": "1.44.0", + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/mini-browser": "1.54.0", + "@theia/monaco": "1.54.0", "@types/highlight.js": "^10.1.0", "@types/markdown-it-anchor": "^4.0.1", "highlight.js": "10.4.1", - "markdown-it-anchor": "~5.0.0" + "markdown-it-anchor": "~5.0.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -45,7 +46,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/preview/src/browser/preview-contribution.ts b/packages/preview/src/browser/preview-contribution.ts index 18f105327ba91..048af03188dbf 100644 --- a/packages/preview/src/browser/preview-contribution.ts +++ b/packages/preview/src/browser/preview-contribution.ts @@ -163,7 +163,10 @@ export class PreviewContribution extends NavigatableWidgetOpenHandler { const { editor } = await this.openSource(ref); editor.revealPosition(location.range.start); - editor.selection = location.range; + editor.selection = { + ...location.range, + direction: 'ltr' + }; ref.revealForSourceLine(location.range.start.line); }); ref.disposed.connect(() => disposable.dispose()); diff --git a/packages/process/package.json b/packages/process/package.json index ee4fe3cb628d1..bef30552ed384 100644 --- a/packages/process/package.json +++ b/packages/process/package.json @@ -1,11 +1,12 @@ { "name": "@theia/process", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia process support.", "dependencies": { - "@theia/core": "1.44.0", - "node-pty": "0.11.0-beta17", - "string-argv": "^0.1.1" + "@theia/core": "1.54.0", + "node-pty": "0.11.0-beta24", + "string-argv": "^0.1.1", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -44,7 +45,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/process/src/node/pseudo-pty.ts b/packages/process/src/node/pseudo-pty.ts index 245a66dd2b211..b44cb5f7a126b 100644 --- a/packages/process/src/node/pseudo-pty.ts +++ b/packages/process/src/node/pseudo-pty.ts @@ -51,4 +51,6 @@ export class PseudoPty implements IPty { pause(): void { } resume(): void { } + + clear(): void { } } diff --git a/packages/property-view/package.json b/packages/property-view/package.json index 96b2afefb22f0..515ea5bbd4ad9 100644 --- a/packages/property-view/package.json +++ b/packages/property-view/package.json @@ -1,10 +1,11 @@ { "name": "@theia/property-view", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Property View Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/filesystem": "1.44.0" + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -39,7 +40,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/remote/package.json b/packages/remote/package.json index 11193dbdeceef..833bccffc3cd2 100644 --- a/packages/remote/package.json +++ b/packages/remote/package.json @@ -1,22 +1,22 @@ { "name": "@theia/remote", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Remote", "dependencies": { - "@theia/core": "1.44.0", - "@theia/filesystem": "1.44.0", + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", "archiver": "^5.3.1", "decompress": "^4.2.1", "decompress-tar": "^4.0.0", "decompress-targz": "^4.0.0", "decompress-unzip": "^4.0.1", - "express-http-proxy": "^1.6.3", + "express-http-proxy": "^2.1.1", "glob": "^8.1.0", - "ssh2": "^1.12.0", - "ssh2-sftp-client": "^9.1.0", "socket.io": "^4.5.3", "socket.io-client": "^4.5.3", - "uuid": "^8.0.0" + "ssh2": "^1.15.0", + "ssh2-sftp-client": "^9.1.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -52,10 +52,10 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0", + "@theia/ext-scripts": "1.54.0", "@types/archiver": "^5.3.2", "@types/decompress": "^4.2.4", - "@types/express-http-proxy": "^1.6.3", + "@types/express-http-proxy": "^1.6.6", "@types/glob": "^8.1.0", "@types/ssh2": "^1.11.11", "@types/ssh2-sftp-client": "^9.0.0" diff --git a/packages/remote/src/electron-browser/port-forwarding/port-forwading-contribution.ts b/packages/remote/src/electron-browser/port-forwarding/port-forwading-contribution.ts new file mode 100644 index 0000000000000..0f45e5465abf1 --- /dev/null +++ b/packages/remote/src/electron-browser/port-forwarding/port-forwading-contribution.ts @@ -0,0 +1,33 @@ +// ***************************************************************************** +// 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 { nls } from '@theia/core'; +import { AbstractViewContribution } from '@theia/core/lib/browser'; +import { injectable } from '@theia/core/shared/inversify'; +import { PortForwardingWidget, PORT_FORWARDING_WIDGET_ID } from './port-forwarding-widget'; + +@injectable() +export class PortForwardingContribution extends AbstractViewContribution { + constructor() { + super({ + widgetId: PORT_FORWARDING_WIDGET_ID, + widgetName: nls.localizeByDefault('Ports'), + defaultWidgetOptions: { + area: 'bottom' + } + }); + } +} diff --git a/packages/remote/src/electron-browser/port-forwarding/port-forwarding-service.ts b/packages/remote/src/electron-browser/port-forwarding/port-forwarding-service.ts new file mode 100644 index 0000000000000..7a5a0c58bfe33 --- /dev/null +++ b/packages/remote/src/electron-browser/port-forwarding/port-forwarding-service.ts @@ -0,0 +1,92 @@ +// ***************************************************************************** +// 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 { Emitter } from '@theia/core'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { RemotePortForwardingProvider } from '../../electron-common/remote-port-forwarding-provider'; + +export interface ForwardedPort { + localPort?: number; + address?: string; + origin?: string; + editing: boolean; +} + +@injectable() +export class PortForwardingService { + + @inject(RemotePortForwardingProvider) + readonly provider: RemotePortForwardingProvider; + + protected readonly onDidChangePortsEmitter = new Emitter(); + readonly onDidChangePorts = this.onDidChangePortsEmitter.event; + + forwardedPorts: ForwardedPort[] = []; + + @postConstruct() + init(): void { + this.provider.getForwardedPorts().then(ports => { + this.forwardedPorts = ports.map(p => ({ address: p.address, localPort: p.port, editing: false })); + this.onDidChangePortsEmitter.fire(); + }); + } + + forwardNewPort(origin?: string): ForwardedPort { + const index = this.forwardedPorts.push({ editing: true, origin }); + return this.forwardedPorts[index - 1]; + } + + updatePort(port: ForwardedPort, newAdress: string): void { + const connectionPort = new URLSearchParams(location.search).get('port'); + if (!connectionPort) { + // if there is no open remote connection we can't forward a port + return; + } + + const parts = newAdress.split(':'); + if (parts.length === 2) { + port.address = parts[0]; + port.localPort = parseInt(parts[1]); + } else { + port.localPort = parseInt(parts[0]); + } + + port.editing = false; + + this.provider.forwardPort(parseInt(connectionPort), { port: port.localPort!, address: port.address }); + this.onDidChangePortsEmitter.fire(); + } + + removePort(port: ForwardedPort): void { + const index = this.forwardedPorts.indexOf(port); + if (index !== -1) { + this.forwardedPorts.splice(index, 1); + this.provider.portRemoved({ port: port.localPort! }); + this.onDidChangePortsEmitter.fire(); + } + } + + isValidAddress(address: string): boolean { + const match = address.match(/^(.*:)?\d+$/); + if (!match) { + return false; + } + + const port = parseInt(address.includes(':') ? address.split(':')[1] : address); + + return !this.forwardedPorts.some(p => p.localPort === port); + } +} diff --git a/packages/remote/src/electron-browser/port-forwarding/port-forwarding-widget.tsx b/packages/remote/src/electron-browser/port-forwarding/port-forwarding-widget.tsx new file mode 100644 index 0000000000000..9333e43f628cc --- /dev/null +++ b/packages/remote/src/electron-browser/port-forwarding/port-forwarding-widget.tsx @@ -0,0 +1,140 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { ReactNode } from '@theia/core/shared/react'; +import { OpenerService, ReactWidget } from '@theia/core/lib/browser'; +import { nls, URI } from '@theia/core'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { ForwardedPort, PortForwardingService } from './port-forwarding-service'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; + +export const PORT_FORWARDING_WIDGET_ID = 'port-forwarding-widget'; + +@injectable() +export class PortForwardingWidget extends ReactWidget { + + @inject(PortForwardingService) + protected readonly portForwardingService: PortForwardingService; + + @inject(OpenerService) + protected readonly openerService: OpenerService; + + @inject(ClipboardService) + protected readonly clipboardService: ClipboardService; + + @postConstruct() + protected init(): void { + this.id = PORT_FORWARDING_WIDGET_ID; + this.node.tabIndex = -1; + this.title.label = nls.localizeByDefault('Ports'); + this.title.caption = this.title.label; + this.title.closable = true; + this.update(); + + this.portForwardingService.onDidChangePorts(() => this.update()); + } + + protected render(): ReactNode { + if (this.portForwardingService.forwardedPorts.length === 0) { + return

    +

    + {nls.localizeByDefault('No forwarded ports. Forward a port to access your locally running services over the internet.\n[Forward a Port]({0})').split('\n')[0]} +

    + {this.renderForwardPortButton()} +
    ; + } + + return
    + + + + + + + + + + + {this.portForwardingService.forwardedPorts.map(port => ( + + {this.renderPortColumn(port)} + {this.renderAddressColumn(port)} + + + + ))} + {!this.portForwardingService.forwardedPorts.some(port => port.editing) && } + +
    {nls.localizeByDefault('Port')}{nls.localizeByDefault('Address')}{nls.localizeByDefault('Running Process')}{nls.localizeByDefault('Origin')}
    {port.origin ? nls.localizeByDefault(port.origin) : ''}
    {this.renderForwardPortButton()}
    +
    ; + } + + protected renderForwardPortButton(): ReactNode { + return ; + } + + protected renderAddressColumn(port: ForwardedPort): ReactNode { + const address = `${port.address ?? '0.0.0.0'}:${port.localPort}`; + return +
    + { + if (e.ctrlKey) { + const uri = new URI(`http://${address}`); + (await this.openerService.getOpener(uri)).open(uri); + } + }} title={nls.localizeByDefault('Follow link') + ' (ctrl/cmd + click)'}> + {port.localPort ? address : ''} + + { + port.localPort && + { + this.clipboardService.writeText(address); + }}> + } +
    + ; + } + + protected renderPortColumn(port: ForwardedPort): ReactNode { + return port.editing ? + : + +
    + {port.localPort} + { + this.portForwardingService.removePort(port); + this.update(); + }}> +
    + ; + } + +} + +function PortEditingInput({ port, service }: { port: ForwardedPort, service: PortForwardingService }): React.JSX.Element { + const [error, setError] = React.useState(false); + return e.key === 'Enter' && !error && service.updatePort(port, e.currentTarget.value)} + onKeyUp={e => setError(!service.isValidAddress(e.currentTarget.value))}>; + +} diff --git a/packages/remote/src/electron-browser/remote-frontend-contribution.ts b/packages/remote/src/electron-browser/remote-frontend-contribution.ts index 9db5ed2a9f199..8e8d2f13d2fde 100644 --- a/packages/remote/src/electron-browser/remote-frontend-contribution.ts +++ b/packages/remote/src/electron-browser/remote-frontend-contribution.ts @@ -108,9 +108,7 @@ export class RemoteFrontendContribution implements CommandContribution, Frontend protected disconnectRemote(): void { const port = new URLSearchParams(location.search).get('localPort'); if (port) { - this.windowService.reload({ - port - }); + this.windowService.reload({ search: { port } }); } } diff --git a/packages/remote/src/electron-browser/remote-frontend-module.ts b/packages/remote/src/electron-browser/remote-frontend-module.ts index f2ddaf3d9e2d4..599c9ef3906d3 100644 --- a/packages/remote/src/electron-browser/remote-frontend-module.ts +++ b/packages/remote/src/electron-browser/remote-frontend-module.ts @@ -16,7 +16,7 @@ import { bindContributionProvider, CommandContribution } from '@theia/core'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { FrontendApplicationContribution, WebSocketConnectionProvider } from '@theia/core/lib/browser'; +import { bindViewContribution, FrontendApplicationContribution, WidgetFactory } from '@theia/core/lib/browser'; import { RemoteSSHContribution } from './remote-ssh-contribution'; import { RemoteSSHConnectionProvider, RemoteSSHConnectionProviderPath } from '../electron-common/remote-ssh-connection-provider'; import { RemoteFrontendContribution } from './remote-frontend-contribution'; @@ -26,6 +26,12 @@ import { RemoteStatusService, RemoteStatusServicePath } from '../electron-common import { ElectronFileDialogService } from '@theia/filesystem/lib/electron-browser/file-dialog/electron-file-dialog-service'; import { RemoteElectronFileDialogService } from './remote-electron-file-dialog-service'; import { bindRemotePreferences } from './remote-preferences'; +import { PortForwardingWidget, PORT_FORWARDING_WIDGET_ID } from './port-forwarding/port-forwarding-widget'; +import { PortForwardingContribution } from './port-forwarding/port-forwading-contribution'; +import { PortForwardingService } from './port-forwarding/port-forwarding-service'; +import { RemotePortForwardingProvider, RemoteRemotePortForwardingProviderPath } from '../electron-common/remote-port-forwarding-provider'; +import { ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider'; +import '../../src/electron-browser/style/port-forwarding-widget.css'; export default new ContainerModule((bind, _, __, rebind) => { bind(RemoteFrontendContribution).toSelf().inSingletonScope(); @@ -42,8 +48,21 @@ export default new ContainerModule((bind, _, __, rebind) => { bind(RemoteService).toSelf().inSingletonScope(); + bind(PortForwardingWidget).toSelf(); + bind(WidgetFactory).toDynamicValue(context => ({ + id: PORT_FORWARDING_WIDGET_ID, + createWidget: () => context.container.get(PortForwardingWidget) + })); + + bindViewContribution(bind, PortForwardingContribution); + bind(PortForwardingService).toSelf().inSingletonScope(); + bind(RemoteSSHConnectionProvider).toDynamicValue(ctx => - WebSocketConnectionProvider.createLocalProxy(ctx.container, RemoteSSHConnectionProviderPath)).inSingletonScope(); + ServiceConnectionProvider.createLocalProxy(ctx.container, RemoteSSHConnectionProviderPath)).inSingletonScope(); bind(RemoteStatusService).toDynamicValue(ctx => - WebSocketConnectionProvider.createLocalProxy(ctx.container, RemoteStatusServicePath)).inSingletonScope(); + ServiceConnectionProvider.createLocalProxy(ctx.container, RemoteStatusServicePath)).inSingletonScope(); + + bind(RemotePortForwardingProvider).toDynamicValue(ctx => + ServiceConnectionProvider.createLocalProxy(ctx.container, RemoteRemotePortForwardingProviderPath)).inSingletonScope(); + }); diff --git a/packages/remote/src/electron-browser/remote-registry-contribution.ts b/packages/remote/src/electron-browser/remote-registry-contribution.ts index c936aeb833886..735b612f29564 100644 --- a/packages/remote/src/electron-browser/remote-registry-contribution.ts +++ b/packages/remote/src/electron-browser/remote-registry-contribution.ts @@ -16,8 +16,7 @@ import { Command, CommandHandler, Emitter, Event } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { WindowSearchParams } from '@theia/core/lib/common/window'; +import { WindowService, WindowReloadOptions } from '@theia/core/lib/browser/window/window-service'; export const RemoteRegistryContribution = Symbol('RemoteRegistryContribution'); @@ -33,15 +32,19 @@ export abstract class AbstractRemoteRegistryContribution implements RemoteRegist abstract registerRemoteCommands(registry: RemoteRegistry): void; - protected openRemote(port: string, newWindow: boolean): void { + protected openRemote(port: string, newWindow: boolean, workspace?: string): void { const searchParams = new URLSearchParams(location.search); const localPort = searchParams.get('localPort') || searchParams.get('port'); - const options: WindowSearchParams = { - port + const options: WindowReloadOptions = { + search: { port } }; if (localPort) { - options.localPort = localPort; + options.search!.localPort = localPort; } + if (workspace) { + options.hash = workspace; + } + if (newWindow) { this.windowService.openNewDefaultWindow(options); } else { diff --git a/packages/remote/src/electron-browser/style/port-forwarding-widget.css b/packages/remote/src/electron-browser/style/port-forwarding-widget.css new file mode 100644 index 0000000000000..7eafdb5e6af4e --- /dev/null +++ b/packages/remote/src/electron-browser/style/port-forwarding-widget.css @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +.port-table { + width: 100%; + margin: calc(var(--theia-ui-padding) * 2); + table-layout: fixed; +} + +.port-table-header { + text-align: left; +} + +.forward-port-button { + margin-left: 0; + width: 100%; +} + +.button-cell { + display: flex; + padding-right: var(--theia-ui-padding); +} + +.forwarded-address:hover { + cursor: pointer; + text-decoration: underline; +} + +.port-edit-input-error { + outline-color: var(--theia-inputValidation-errorBorder); +} diff --git a/packages/remote/src/electron-common/remote-port-forwarding-provider.ts b/packages/remote/src/electron-common/remote-port-forwarding-provider.ts new file mode 100644 index 0000000000000..a79bd5b9df285 --- /dev/null +++ b/packages/remote/src/electron-common/remote-port-forwarding-provider.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export const RemoteRemotePortForwardingProviderPath = '/remote/port-forwarding'; + +export const RemotePortForwardingProvider = Symbol('RemoteSSHConnectionProvider'); + +export interface ForwardedPort { + port: number; + address?: string; +} + +export interface RemotePortForwardingProvider { + forwardPort(connectionPort: number, portToForward: ForwardedPort): Promise; + portRemoved(port: ForwardedPort): Promise; + getForwardedPorts(): Promise +} diff --git a/packages/remote/src/electron-node/backend-remote-service-impl.ts b/packages/remote/src/electron-node/backend-remote-service-impl.ts index 805481b9eb790..db88035eeb4ef 100644 --- a/packages/remote/src/electron-node/backend-remote-service-impl.ts +++ b/packages/remote/src/electron-node/backend-remote-service-impl.ts @@ -17,7 +17,7 @@ import { CliContribution } from '@theia/core/lib/node'; import { injectable } from '@theia/core/shared/inversify'; import { Arguments, Argv } from '@theia/core/shared/yargs'; -import { BackendRemoteService } from '@theia/core/lib/node/backend-remote-service'; +import { BackendRemoteService } from '@theia/core/lib/node/remote/backend-remote-service'; export const REMOTE_START = 'remote'; diff --git a/packages/remote/src/electron-node/remote-backend-module.ts b/packages/remote/src/electron-node/remote-backend-module.ts index 733929479ce44..ad0084eb8ae79 100644 --- a/packages/remote/src/electron-node/remote-backend-module.ts +++ b/packages/remote/src/electron-node/remote-backend-module.ts @@ -27,21 +27,28 @@ import { RemoteCopyService } from './setup/remote-copy-service'; import { RemoteSetupService } from './setup/remote-setup-service'; import { RemoteNativeDependencyService } from './setup/remote-native-dependency-service'; import { BackendRemoteServiceImpl } from './backend-remote-service-impl'; -import { BackendRemoteService } from '@theia/core/lib/node/backend-remote-service'; +import { BackendRemoteService } from '@theia/core/lib/node/remote/backend-remote-service'; import { RemoteNodeSetupService } from './setup/remote-node-setup-service'; import { RemotePosixScriptStrategy, RemoteSetupScriptService, RemoteWindowsScriptStrategy } from './setup/remote-setup-script-service'; import { RemoteStatusService, RemoteStatusServicePath } from '../electron-common/remote-status-service'; import { RemoteStatusServiceImpl } from './remote-status-service'; import { ConnectionHandler, RpcConnectionHandler, bindContributionProvider } from '@theia/core'; -import { RemoteCopyContribution, RemoteCopyRegistry } from './setup/remote-copy-contribution'; +import { RemoteCopyRegistryImpl } from './setup/remote-copy-contribution'; +import { RemoteCopyContribution } from '@theia/core/lib/node/remote/remote-copy-contribution'; import { MainCopyContribution } from './setup/main-copy-contribution'; import { RemoteNativeDependencyContribution } from './setup/remote-native-dependency-contribution'; import { AppNativeDependencyContribution } from './setup/app-native-dependency-contribution'; +import { RemotePortForwardingProviderImpl } from './remote-port-forwarding-provider'; +import { RemotePortForwardingProvider, RemoteRemotePortForwardingProviderPath } from '../electron-common/remote-port-forwarding-provider'; export const remoteConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { bind(RemoteSSHConnectionProviderImpl).toSelf().inSingletonScope(); bind(RemoteSSHConnectionProvider).toService(RemoteSSHConnectionProviderImpl); bindBackendService(RemoteSSHConnectionProviderPath, RemoteSSHConnectionProvider); + + bind(RemotePortForwardingProviderImpl).toSelf().inSingletonScope(); + bind(RemotePortForwardingProvider).toService(RemotePortForwardingProviderImpl); + bindBackendService(RemoteRemotePortForwardingProviderPath, RemotePortForwardingProvider); }); export default new ContainerModule((bind, _unbind, _isBound, rebind) => { @@ -62,7 +69,7 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(RemotePosixScriptStrategy).toSelf().inSingletonScope(); bind(RemoteSetupScriptService).toSelf().inSingletonScope(); bind(RemoteNativeDependencyService).toSelf().inSingletonScope(); - bind(RemoteCopyRegistry).toSelf().inSingletonScope(); + bind(RemoteCopyRegistryImpl).toSelf().inSingletonScope(); bindContributionProvider(bind, RemoteCopyContribution); bindContributionProvider(bind, RemoteNativeDependencyContribution); bind(MainCopyContribution).toSelf().inSingletonScope(); diff --git a/packages/remote/src/electron-node/remote-connection-service.ts b/packages/remote/src/electron-node/remote-connection-service.ts index ce776b8590fc4..e86612ee60257 100644 --- a/packages/remote/src/electron-node/remote-connection-service.ts +++ b/packages/remote/src/electron-node/remote-connection-service.ts @@ -18,8 +18,8 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { RemoteConnection } from './remote-types'; import { Disposable } from '@theia/core'; import { RemoteCopyService } from './setup/remote-copy-service'; -import { RemoteNativeDependencyService } from './setup/remote-native-dependency-service'; import { BackendApplicationContribution } from '@theia/core/lib/node'; +import { RemoteSetupService } from './setup/remote-setup-service'; @injectable() export class RemoteConnectionService implements BackendApplicationContribution { @@ -27,8 +27,9 @@ export class RemoteConnectionService implements BackendApplicationContribution { @inject(RemoteCopyService) protected readonly copyService: RemoteCopyService; - @inject(RemoteNativeDependencyService) - protected readonly nativeDependencyService: RemoteNativeDependencyService; + // Workaround for the fact that connection scoped services cannot directly inject these services. + @inject(RemoteSetupService) + protected readonly remoteSetupService: RemoteSetupService; protected readonly connections = new Map(); diff --git a/packages/remote/src/electron-node/remote-port-forwarding-provider.ts b/packages/remote/src/electron-node/remote-port-forwarding-provider.ts new file mode 100644 index 0000000000000..6c5fbb9b68b48 --- /dev/null +++ b/packages/remote/src/electron-node/remote-port-forwarding-provider.ts @@ -0,0 +1,66 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { ForwardedPort, RemotePortForwardingProvider } from '../electron-common/remote-port-forwarding-provider'; +import { createServer, Server } from 'net'; +import { RemoteConnectionService } from './remote-connection-service'; +import { RemoteConnection } from './remote-types'; + +interface ForwardInfo { + connection: RemoteConnection + port: ForwardedPort + server: Server +} + +@injectable() +export class RemotePortForwardingProviderImpl implements RemotePortForwardingProvider { + + @inject(RemoteConnectionService) + protected readonly connectionService: RemoteConnectionService; + + protected static forwardedPorts: ForwardInfo[] = []; + + async forwardPort(connectionPort: number, portToForward: ForwardedPort): Promise { + const currentConnection = this.connectionService.getConnectionFromPort(connectionPort); + if (!currentConnection) { + throw new Error(`No connection found for port ${connectionPort}`); + } + + const server = createServer(socket => { + currentConnection?.forwardOut(socket, portToForward.port); + }).listen(portToForward.port, portToForward.address); + + currentConnection.onDidDisconnect(() => { + this.portRemoved(portToForward); + }); + + RemotePortForwardingProviderImpl.forwardedPorts.push({ connection: currentConnection, port: portToForward, server }); + } + + async portRemoved(forwardedPort: ForwardedPort): Promise { + const forwardInfo = RemotePortForwardingProviderImpl.forwardedPorts.find(info => info.port.port === forwardedPort.port); + if (forwardInfo) { + forwardInfo.server.close(); + RemotePortForwardingProviderImpl.forwardedPorts.splice(RemotePortForwardingProviderImpl.forwardedPorts.indexOf(forwardInfo), 1); + } + } + + async getForwardedPorts(): Promise { + return Array.from(RemotePortForwardingProviderImpl.forwardedPorts) + .map(forwardInfo => ({ ...forwardInfo.port, editing: false })); + } +} diff --git a/packages/remote/src/electron-node/remote-types.ts b/packages/remote/src/electron-node/remote-types.ts index 7499fd682004d..c8ab798b1a15a 100644 --- a/packages/remote/src/electron-node/remote-types.ts +++ b/packages/remote/src/electron-node/remote-types.ts @@ -14,14 +14,9 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Disposable, Event, OS } from '@theia/core'; +import { Disposable, Event } from '@theia/core'; import * as net from 'net'; -export interface RemotePlatform { - os: OS.Type - arch: string -} - export type RemoteStatusReport = (message: string) => void; export interface ExpressLayer { @@ -49,8 +44,21 @@ export interface RemoteConnection extends Disposable { localPort: number; remotePort: number; onDidDisconnect: Event; - forwardOut(socket: net.Socket): void; + forwardOut(socket: net.Socket, port?: number): void; + + /** + * execute a single command on the remote machine + */ exec(cmd: string, args?: string[], options?: RemoteExecOptions): Promise; + + /** + * execute a command on the remote machine and wait for a specific output + * @param tester function which returns true if the output is as expected + */ execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options?: RemoteExecOptions): Promise; + + /** + * copy files from local to remote + */ copy(localPath: string | Buffer | NodeJS.ReadableStream, remotePath: string): Promise; } diff --git a/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts index 1e0d8e52719cb..4e0ffbd3e365a 100644 --- a/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts +++ b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts @@ -16,7 +16,7 @@ import { injectable } from '@theia/core/shared/inversify'; import { RemoteNativeDependencyContribution, DownloadOptions, DependencyDownload } from './remote-native-dependency-contribution'; -import { RemotePlatform } from '../remote-types'; +import { RemotePlatform } from '@theia/core/lib/node/remote/remote-cli-contribution'; import { OS } from '@theia/core'; @injectable() diff --git a/packages/remote/src/electron-node/setup/main-copy-contribution.ts b/packages/remote/src/electron-node/setup/main-copy-contribution.ts index e615af69b33cf..452d0eb15409c 100644 --- a/packages/remote/src/electron-node/setup/main-copy-contribution.ts +++ b/packages/remote/src/electron-node/setup/main-copy-contribution.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { injectable } from '@theia/core/shared/inversify'; -import { RemoteCopyContribution, RemoteCopyRegistry } from './remote-copy-contribution'; +import { RemoteCopyContribution, RemoteCopyRegistry } from '@theia/core/lib/node/remote/remote-copy-contribution'; @injectable() export class MainCopyContribution implements RemoteCopyContribution { diff --git a/packages/remote/src/electron-node/setup/remote-copy-contribution.ts b/packages/remote/src/electron-node/setup/remote-copy-contribution.ts index b6db142310ea8..9c9130e8c48f9 100644 --- a/packages/remote/src/electron-node/setup/remote-copy-contribution.ts +++ b/packages/remote/src/electron-node/setup/remote-copy-contribution.ts @@ -16,36 +16,15 @@ import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; import { inject, injectable } from '@theia/core/shared/inversify'; +import { RemoteCopyRegistry, RemoteFile, RemoteCopyOptions } from '@theia/core/lib/node/remote/remote-copy-contribution'; import { glob as globCallback } from 'glob'; import { promisify } from 'util'; import * as path from 'path'; -import { MaybePromise } from '@theia/core'; const promiseGlob = promisify(globCallback); -export const RemoteCopyContribution = Symbol('RemoteCopyContribution'); - -export interface RemoteCopyContribution { - copy(registry: RemoteCopyRegistry): MaybePromise -} - -export interface RemoteCopyOptions { - /** - * The mode that the file should be set to once copied to the remote. - * - * Only relevant for POSIX-like systems - */ - mode?: number; -} - -export interface RemoteFile { - path: string - target: string - options?: RemoteCopyOptions; -} - @injectable() -export class RemoteCopyRegistry { +export class RemoteCopyRegistryImpl implements RemoteCopyRegistry { @inject(ApplicationPackage) protected readonly applicationPackage: ApplicationPackage; @@ -57,15 +36,16 @@ export class RemoteCopyRegistry { } async glob(pattern: string, target?: string): Promise { + return this.doGlob(pattern, this.applicationPackage.projectPath, target); + } + + async doGlob(pattern: string, cwd: string, target?: string): Promise { const projectPath = this.applicationPackage.projectPath; - const globResult = await promiseGlob(pattern, { - cwd: projectPath - }); - const relativeFiles = globResult.map(file => path.relative(projectPath, file)); - for (const file of relativeFiles) { + const globResult = await promiseGlob(pattern, { cwd, nodir: true }); + for (const file of globResult) { const targetFile = this.withTarget(file, target); this.files.push({ - path: file, + path: path.relative(projectPath, path.resolve(cwd, file)), target: targetFile }); } @@ -81,7 +61,11 @@ export class RemoteCopyRegistry { } async directory(dir: string, target?: string): Promise { - return this.glob(dir + '/**', target); + let absoluteDir = dir; + if (!path.isAbsolute(absoluteDir)) { + absoluteDir = path.join(this.applicationPackage.projectPath, dir); + } + return this.doGlob('**/*', absoluteDir, target ?? dir); } protected withTarget(file: string, target?: string): string { diff --git a/packages/remote/src/electron-node/setup/remote-copy-service.ts b/packages/remote/src/electron-node/setup/remote-copy-service.ts index ffe46d40f99e4..4d6200c22f263 100644 --- a/packages/remote/src/electron-node/setup/remote-copy-service.ts +++ b/packages/remote/src/electron-node/setup/remote-copy-service.ts @@ -20,10 +20,12 @@ import * as fs from 'fs'; import * as os from 'os'; import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; import { inject, injectable, named } from '@theia/core/shared/inversify'; -import { RemoteConnection, RemotePlatform } from '../remote-types'; +import { RemoteConnection } from '../remote-types'; +import { RemotePlatform } from '@theia/core/lib/node/remote/remote-cli-contribution'; import { RemoteNativeDependencyService } from './remote-native-dependency-service'; import { ContributionProvider } from '@theia/core'; -import { RemoteCopyContribution, RemoteCopyRegistry, RemoteFile } from './remote-copy-contribution'; +import { RemoteCopyRegistryImpl } from './remote-copy-contribution'; +import { RemoteCopyContribution, RemoteFile } from '@theia/core/lib/node/remote/remote-copy-contribution'; @injectable() export class RemoteCopyService { @@ -31,8 +33,8 @@ export class RemoteCopyService { @inject(ApplicationPackage) protected readonly applicationPackage: ApplicationPackage; - @inject(RemoteCopyRegistry) - protected readonly copyRegistry: RemoteCopyRegistry; + @inject(RemoteCopyRegistryImpl) + protected readonly copyRegistry: RemoteCopyRegistryImpl; @inject(RemoteNativeDependencyService) protected readonly nativeDependencyService: RemoteNativeDependencyService; diff --git a/packages/remote/src/electron-node/setup/remote-native-dependency-contribution.ts b/packages/remote/src/electron-node/setup/remote-native-dependency-contribution.ts index ece62ba8c45a5..3a5260645658f 100644 --- a/packages/remote/src/electron-node/setup/remote-native-dependency-contribution.ts +++ b/packages/remote/src/electron-node/setup/remote-native-dependency-contribution.ts @@ -16,7 +16,7 @@ import { isObject } from '@theia/core'; import { RequestOptions } from '@theia/core/shared/@theia/request'; -import { RemotePlatform } from '../remote-types'; +import { RemotePlatform } from '@theia/core/lib/node/remote/remote-cli-contribution'; export interface FileDependencyResult { path: string; diff --git a/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts b/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts index 6caa905cbdaa5..579ccd5437327 100644 --- a/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts +++ b/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts @@ -21,7 +21,7 @@ import * as decompress from 'decompress'; import * as path from 'path'; import * as fs from 'fs/promises'; import { DependencyDownload, DirectoryDependencyDownload, RemoteNativeDependencyContribution } from './remote-native-dependency-contribution'; -import { RemotePlatform } from '../remote-types'; +import { RemotePlatform } from '@theia/core/lib/node/remote/remote-cli-contribution'; const decompressTar = require('decompress-tar'); const decompressTargz = require('decompress-targz'); diff --git a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts index f559c2c494cde..49567e537736a 100644 --- a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts @@ -21,7 +21,7 @@ import * as os from 'os'; import { inject, injectable } from '@theia/core/shared/inversify'; import { RequestService } from '@theia/core/shared/@theia/request'; import { RemoteSetupScriptService } from './remote-setup-script-service'; -import { RemotePlatform } from '../remote-types'; +import { RemotePlatform } from '@theia/core/lib/node/remote/remote-cli-contribution'; import { OS } from '@theia/core'; /** diff --git a/packages/remote/src/electron-node/setup/remote-setup-script-service.ts b/packages/remote/src/electron-node/setup/remote-setup-script-service.ts index b6a7011d59ea3..afa606283c2ac 100644 --- a/packages/remote/src/electron-node/setup/remote-setup-script-service.ts +++ b/packages/remote/src/electron-node/setup/remote-setup-script-service.ts @@ -16,7 +16,7 @@ import { OS } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { RemotePlatform } from '../remote-types'; +import { RemotePlatform } from '@theia/core/lib/node/remote/remote-cli-contribution'; export interface RemoteScriptStrategy { exec(): string; diff --git a/packages/remote/src/electron-node/setup/remote-setup-service.ts b/packages/remote/src/electron-node/setup/remote-setup-service.ts index 16aee1c0ae085..9ef150807d985 100644 --- a/packages/remote/src/electron-node/setup/remote-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-setup-service.ts @@ -14,12 +14,13 @@ // 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 { RemoteConnection, RemoteExecResult, RemotePlatform, RemoteStatusReport } from '../remote-types'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { RemoteConnection, RemoteExecResult, RemoteStatusReport } from '../remote-types'; +import { RemoteCliContext, RemoteCliContribution, RemotePlatform } from '@theia/core/lib/node/remote/remote-cli-contribution'; import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; import { RemoteCopyService } from './remote-copy-service'; import { RemoteNativeDependencyService } from './remote-native-dependency-service'; -import { OS, THEIA_VERSION } from '@theia/core'; +import { ContributionProvider, OS, THEIA_VERSION } from '@theia/core'; import { RemoteNodeSetupService } from './remote-node-setup-service'; import { RemoteSetupScriptService } from './remote-setup-script-service'; @@ -29,6 +30,11 @@ export interface RemoteSetupOptions { nodeDownloadTemplate?: string; } +export interface RemoteSetupResult { + applicationDirectory: string; + nodeDirectory: string; +} + @injectable() export class RemoteSetupService { @@ -47,7 +53,10 @@ export class RemoteSetupService { @inject(ApplicationPackage) protected readonly applicationPackage: ApplicationPackage; - async setup(options: RemoteSetupOptions): Promise { + @inject(ContributionProvider) @named(RemoteCliContribution) + protected readonly cliContributions: ContributionProvider; + + async setup(options: RemoteSetupOptions): Promise { const { connection, report, @@ -86,22 +95,36 @@ export class RemoteSetupService { report('Starting application on remote...'); const port = await this.startApplication(connection, platform, applicationDirectory, remoteNodeDirectory); connection.remotePort = port; + return { + applicationDirectory: libDir, + nodeDirectory: remoteNodeDirectory + }; } protected async startApplication(connection: RemoteConnection, platform: RemotePlatform, remotePath: string, nodeDir: string): Promise { const nodeExecutable = this.scriptService.joinPath(platform, nodeDir, ...(platform.os === OS.Type.Windows ? ['node.exe'] : ['bin', 'node'])); const mainJsFile = this.scriptService.joinPath(platform, remotePath, 'lib', 'backend', 'main.js'); - const localAddressRegex = /listening on http:\/\/127.0.0.1:(\d+)/; + const localAddressRegex = /listening on http:\/\/0.0.0.0:(\d+)/; let prefix = ''; if (platform.os === OS.Type.Windows) { // We might to switch to PowerShell beforehand on Windows prefix = this.scriptService.exec(platform) + ' '; } + const remoteContext: RemoteCliContext = { + platform, + directory: remotePath + }; + const args: string[] = ['--hostname=0.0.0.0', `--port=${connection.remotePort ?? 0}`, '--remote']; + for (const cli of this.cliContributions.getContributions()) { + if (cli.enhanceArgs) { + args.push(...await cli.enhanceArgs(remoteContext)); + } + } // Change to the remote application path and start a node process with the copied main.js file // This way, our current working directory is set as expected const result = await connection.execPartial(`${prefix}cd "${remotePath}";${nodeExecutable}`, stdout => localAddressRegex.test(stdout), - [mainJsFile, '--hostname=127.0.0.1', '--port=0', '--remote']); + [mainJsFile, ...args]); const match = localAddressRegex.exec(result.stdout); if (!match) { diff --git a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts index 2f942de3c9a0f..7c9342c0dd8ff 100644 --- a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts +++ b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts @@ -27,7 +27,7 @@ import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester import { Deferred, timeout } from '@theia/core/lib/common/promise-util'; import { SSHIdentityFileCollector, SSHKey } from './ssh-identity-file-collector'; import { RemoteSetupService } from '../setup/remote-setup-service'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; @injectable() export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvider { @@ -86,13 +86,14 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi const deferred = new Deferred(); const sshClient = new ssh2.Client(); const identityFiles = await this.identityFileCollector.gatherIdentityFiles(); - const sshAuthHandler = this.getAuthHandler(user, host, identityFiles); + const hostUrl = new URL(`ssh://${host}`); + const sshAuthHandler = this.getAuthHandler(user, hostUrl.hostname, identityFiles); sshClient .on('ready', async () => { const connection = new RemoteSSHConnection({ client: sshClient, - id: v4(), - name: host, + id: generateUuid(), + name: hostUrl.hostname, type: 'SSH' }); try { @@ -102,11 +103,12 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi deferred.reject(err); } }).on('end', () => { - console.log(`Ended remote connection to host '${user}@${host}'`); + console.log(`Ended remote connection to host '${user}@${hostUrl.hostname}'`); }).on('error', err => { deferred.reject(err); }).connect({ - host: host, + host: hostUrl.hostname, + port: hostUrl.port ? parseInt(hostUrl.port, 10) : undefined, username: user, authHandler: (methodsLeft, successes, callback) => (sshAuthHandler(methodsLeft, successes, callback), undefined) }); @@ -276,8 +278,8 @@ export class RemoteSSHConnection implements RemoteConnection { return sftpClient; } - forwardOut(socket: net.Socket): void { - this.client.forwardOut(socket.localAddress!, socket.localPort!, '127.0.0.1', this.remotePort, (err, stream) => { + forwardOut(socket: net.Socket, port?: number): void { + this.client.forwardOut(socket.localAddress!, socket.localPort!, '127.0.0.1', port ?? this.remotePort, (err, stream) => { if (err) { console.debug('Proxy message rejected', err); } else { diff --git a/packages/scm-extra/package.json b/packages/scm-extra/package.json index b2725c136adfe..227b1f92580cd 100644 --- a/packages/scm-extra/package.json +++ b/packages/scm-extra/package.json @@ -1,13 +1,14 @@ { "name": "@theia/scm-extra", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Source control extras Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/navigator": "1.44.0", - "@theia/scm": "1.44.0" + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/navigator": "1.54.0", + "@theia/scm": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -42,7 +43,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/scm/package.json b/packages/scm/package.json index e398d924d66e4..87b52c2704627 100644 --- a/packages/scm/package.json +++ b/packages/scm/package.json @@ -1,16 +1,19 @@ { "name": "@theia/scm", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Source control Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@types/diff": "^3.2.2", - "diff": "^3.4.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", + "@types/diff": "^5.2.1", + "diff": "^5.2.0", "p-debounce": "^2.1.0", "react-autosize-textarea": "^7.0.0", - "ts-md5": "^1.2.2" + "ts-md5": "^1.2.2", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -42,10 +45,11 @@ "compile": "theiaext compile", "docs": "theiaext docs", "lint": "theiaext lint", + "test": "theiaext test", "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/scm/src/browser/decorations/scm-decorations-service.ts b/packages/scm/src/browser/decorations/scm-decorations-service.ts index 53dd72eb16341..c597dd91385dc 100644 --- a/packages/scm/src/browser/decorations/scm-decorations-service.ts +++ b/packages/scm/src/browser/decorations/scm-decorations-service.ts @@ -15,64 +15,88 @@ // ***************************************************************************** import { injectable, inject } from '@theia/core/shared/inversify'; -import { ResourceProvider } from '@theia/core'; -import { DirtyDiffDecorator } from '../dirty-diff/dirty-diff-decorator'; +import { DisposableCollection, Emitter, Event, ResourceProvider } from '@theia/core'; +import { DirtyDiffDecorator, DirtyDiffUpdate } from '../dirty-diff/dirty-diff-decorator'; import { DiffComputer } from '../dirty-diff/diff-computer'; import { ContentLines } from '../dirty-diff/content-lines'; -import { EditorManager, TextEditor } from '@theia/editor/lib/browser'; +import { EditorManager, EditorWidget, TextEditor } from '@theia/editor/lib/browser'; import { ScmService } from '../scm-service'; +import throttle = require('@theia/core/shared/lodash.throttle'); + @injectable() export class ScmDecorationsService { - private readonly diffComputer: DiffComputer; - private dirtyState: boolean = true; + private readonly diffComputer = new DiffComputer(); + + protected readonly onDirtyDiffUpdateEmitter = new Emitter(); + readonly onDirtyDiffUpdate: Event = this.onDirtyDiffUpdateEmitter.event; - constructor(@inject(DirtyDiffDecorator) protected readonly decorator: DirtyDiffDecorator, + constructor( + @inject(DirtyDiffDecorator) protected readonly decorator: DirtyDiffDecorator, @inject(ScmService) protected readonly scmService: ScmService, @inject(EditorManager) protected readonly editorManager: EditorManager, - @inject(ResourceProvider) protected readonly resourceProvider: ResourceProvider) { - this.diffComputer = new DiffComputer(); - this.editorManager.onCreated(async editor => this.applyEditorDecorations(editor.editor)); - this.scmService.onDidAddRepository(repository => repository.provider.onDidChange(() => { - const editor = this.editorManager.currentEditor; - if (editor) { - if (this.dirtyState) { - this.applyEditorDecorations(editor.editor); - this.dirtyState = false; - } else { - /** onDidChange event might be called several times one after another, so need to prevent repeated events. */ - setTimeout(() => { - this.dirtyState = true; - }, 500); - } + @inject(ResourceProvider) protected readonly resourceProvider: ResourceProvider + ) { + const updateTasks = new Map(); + this.editorManager.onCreated(editorWidget => { + const { editor } = editorWidget; + if (!this.supportsDirtyDiff(editor)) { + return; } - })); - this.scmService.onDidChangeSelectedRepository(() => { - const editor = this.editorManager.currentEditor; - if (editor) { - this.applyEditorDecorations(editor.editor); + const toDispose = new DisposableCollection(); + const updateTask = this.createUpdateTask(editor); + updateTasks.set(editorWidget, updateTask); + toDispose.push(editor.onDocumentContentChanged(() => updateTask())); + editorWidget.disposed.connect(() => { + updateTask.cancel(); + updateTasks.delete(editorWidget); + toDispose.dispose(); + }); + updateTask(); + }); + const runUpdateTasks = () => { + for (const updateTask of updateTasks.values()) { + updateTask(); } + }; + this.scmService.onDidAddRepository(({ provider }) => { + provider.onDidChange(runUpdateTasks); + provider.onDidChangeResources?.(runUpdateTasks); }); + this.scmService.onDidChangeSelectedRepository(runUpdateTasks); } async applyEditorDecorations(editor: TextEditor): Promise { const currentRepo = this.scmService.selectedRepository; if (currentRepo) { try { - const uri = editor.uri.withScheme(currentRepo.provider.id).withQuery(`{"ref":"", "path":"${editor.uri.path.toString()}"}`); + // Currently, the uri used here is specific to vscode.git; other SCM providers are thus not supported. + // See https://github.com/eclipse-theia/theia/pull/13104#discussion_r1494540628 for a detailed discussion. + const query = { path: editor.uri['codeUri'].fsPath, ref: '~' }; + const uri = editor.uri.withScheme(currentRepo.provider.id).withQuery(JSON.stringify(query)); const previousResource = await this.resourceProvider(uri); - const previousContent = await previousResource.readContents(); - const previousLines = ContentLines.fromString(previousContent); - const currentResource = await this.resourceProvider(editor.uri); - const currentContent = await currentResource.readContents(); - const currentLines = ContentLines.fromString(currentContent); - const { added, removed, modified } = this.diffComputer.computeDirtyDiff(ContentLines.arrayLike(previousLines), ContentLines.arrayLike(currentLines)); - this.decorator.applyDecorations({ editor: editor, added, removed, modified }); - currentResource.dispose(); - previousResource.dispose(); + try { + const previousContent = await previousResource.readContents(); + const previousLines = ContentLines.fromString(previousContent); + const currentLines = ContentLines.fromTextEditorDocument(editor.document); + const dirtyDiff = this.diffComputer.computeDirtyDiff(ContentLines.arrayLike(previousLines), ContentLines.arrayLike(currentLines)); + const update = { editor, previousRevisionUri: uri, ...dirtyDiff }; + this.decorator.applyDecorations(update); + this.onDirtyDiffUpdateEmitter.fire(update); + } finally { + previousResource.dispose(); + } } catch (e) { // Scm resource may not be found, do nothing. } } } + + protected supportsDirtyDiff(editor: TextEditor): boolean { + return editor.shouldDisplayDirtyDiff(); + } + + protected createUpdateTask(editor: TextEditor): { (): void; cancel(): void; } { + return throttle(() => this.applyEditorDecorations(editor), 500); + } } diff --git a/packages/scm/src/browser/dirty-diff/content-lines.ts b/packages/scm/src/browser/dirty-diff/content-lines.ts index d3c0a2207ec4d..2e0b40cfbb187 100644 --- a/packages/scm/src/browser/dirty-diff/content-lines.ts +++ b/packages/scm/src/browser/dirty-diff/content-lines.ts @@ -14,6 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { TextEditorDocument } from '@theia/editor/lib/browser'; + export interface ContentLines extends ArrayLike { readonly length: number, getLineContent: (line: number) => string, @@ -65,6 +67,13 @@ export namespace ContentLines { }; } + export function fromTextEditorDocument(document: TextEditorDocument): ContentLines { + return { + length: document.lineCount, + getLineContent: line => document.getLineContent(line + 1), + }; + } + export function arrayLike(lines: ContentLines): ContentLinesArrayLike { return new Proxy(lines as ContentLines, getProxyHandler()) as ContentLinesArrayLike; } diff --git a/packages/scm/src/browser/dirty-diff/diff-computer.spec.ts b/packages/scm/src/browser/dirty-diff/diff-computer.spec.ts index 34fa24e51a27c..62ebf18cd51f5 100644 --- a/packages/scm/src/browser/dirty-diff/diff-computer.spec.ts +++ b/packages/scm/src/browser/dirty-diff/diff-computer.spec.ts @@ -42,9 +42,12 @@ describe('dirty-diff-computer', () => { ], ); expect(dirtyDiff).to.be.deep.equal({ - added: [], - modified: [], - removed: [0], + changes: [ + { + previousRange: { start: 1, end: 2 }, + currentRange: { start: 1, end: 1 }, + }, + ], }); }); @@ -56,22 +59,29 @@ describe('dirty-diff-computer', () => { sequenceOfN(2), ); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [1], - added: [], + changes: [ + { + previousRange: { start: 2, end: 2 + lines }, + currentRange: { start: 2, end: 2 }, + }, + ], }); }); }); it('remove all lines', () => { + const numberOfLines = 10; const dirtyDiff = computeDirtyDiff( - sequenceOfN(10, () => 'TO-BE-REMOVED'), + sequenceOfN(numberOfLines, () => 'TO-BE-REMOVED'), [''] ); expect(dirtyDiff).to.be.deep.equal({ - added: [], - modified: [], - removed: [0], + changes: [ + { + previousRange: { start: 0, end: numberOfLines }, + currentRange: { start: 0, end: 0 }, + }, + ], }); }); @@ -83,9 +93,12 @@ describe('dirty-diff-computer', () => { sequenceOfN(2), ); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [0], - added: [], + changes: [ + { + previousRange: { start: 0, end: lines }, + currentRange: { start: 0, end: 0 }, + }, + ], }); }); }); @@ -96,9 +109,12 @@ describe('dirty-diff-computer', () => { const modified = insertIntoArray(previous, 2, ...sequenceOfN(lines, () => 'ADDED LINE')); const dirtyDiff = computeDirtyDiff(previous, modified); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [], - added: [{ start: 2, end: 2 + lines - 1 }], + changes: [ + { + previousRange: { start: 2, end: 2 }, + currentRange: { start: 2, end: 2 + lines }, + }, + ], }); }); }); @@ -111,9 +127,12 @@ describe('dirty-diff-computer', () => { .concat(sequenceOfN(2)) ); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [], - added: [{ start: 0, end: lines - 1 }], + changes: [ + { + previousRange: { start: 0, end: 0 }, + currentRange: { start: 0, end: lines }, + }, + ], }); }); }); @@ -125,9 +144,12 @@ describe('dirty-diff-computer', () => { sequenceOfN(numberOfLines, () => 'ADDED LINE') ); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [], - added: [{ start: 0, end: numberOfLines - 1 }], + changes: [ + { + previousRange: { start: 0, end: 0 }, + currentRange: { start: 0, end: numberOfLines }, + }, + ], }); }); @@ -145,9 +167,12 @@ describe('dirty-diff-computer', () => { ] ); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [], - added: [{ start: 1, end: 2 }], + changes: [ + { + previousRange: { start: 1, end: 1 }, + currentRange: { start: 1, end: 3 }, + }, + ], }); }); @@ -162,9 +187,12 @@ describe('dirty-diff-computer', () => { ] ); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [], - added: [{ start: 1, end: 1 }], + changes: [ + { + previousRange: { start: 1, end: 1 }, + currentRange: { start: 1, end: 2 }, + }, + ], }); }); @@ -176,9 +204,12 @@ describe('dirty-diff-computer', () => { .concat(new Array(lines).map(() => '')) ); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [], - added: [{ start: 2, end: 1 + lines }], + changes: [ + { + previousRange: { start: 2, end: 2 }, + currentRange: { start: 2, end: 2 + lines }, + }, + ], }); }); }); @@ -200,9 +231,12 @@ describe('dirty-diff-computer', () => { ] ); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [], - added: [{ start: 1, end: 5 }], + changes: [ + { + previousRange: { start: 1, end: 1 }, + currentRange: { start: 1, end: 6 }, + }, + ], }); }); @@ -213,9 +247,12 @@ describe('dirty-diff-computer', () => { ['0'].concat(sequenceOfN(lines, () => 'ADDED LINE')) ); expect(dirtyDiff).to.be.deep.equal({ - modified: [], - removed: [], - added: [{ start: 1, end: lines }], + changes: [ + { + previousRange: { start: 1, end: 1 }, + currentRange: { start: 1, end: lines + 1 }, + }, + ], }); }); }); @@ -234,9 +271,12 @@ describe('dirty-diff-computer', () => { ] ); expect(dirtyDiff).to.be.deep.equal({ - removed: [], - added: [], - modified: [{ start: 1, end: 1 }], + changes: [ + { + previousRange: { start: 1, end: 2 }, + currentRange: { start: 1, end: 2 }, + }, + ], }); }); @@ -247,9 +287,12 @@ describe('dirty-diff-computer', () => { sequenceOfN(numberOfLines, () => 'MODIFIED') ); expect(dirtyDiff).to.be.deep.equal({ - removed: [], - added: [], - modified: [{ start: 0, end: numberOfLines - 1 }], + changes: [ + { + previousRange: { start: 0, end: numberOfLines }, + currentRange: { start: 0, end: numberOfLines }, + }, + ], }); }); @@ -268,9 +311,12 @@ describe('dirty-diff-computer', () => { ] ); expect(dirtyDiff).to.be.deep.equal({ - removed: [], - added: [], - modified: [{ start: 1, end: 2 }], + changes: [ + { + previousRange: { start: 1, end: 4 }, + currentRange: { start: 1, end: 3 }, + }, + ], }); }); @@ -305,9 +351,20 @@ describe('dirty-diff-computer', () => { ] ); expect(dirtyDiff).to.be.deep.equal({ - removed: [3], - added: [{ start: 10, end: 11 }], - modified: [{ start: 0, end: 0 }], + changes: [ + { + previousRange: { start: 0, end: 1 }, + currentRange: { start: 0, end: 1 }, + }, + { + previousRange: { start: 4, end: 5 }, + currentRange: { start: 4, end: 4 }, + }, + { + previousRange: { start: 11, end: 11 }, + currentRange: { start: 10, end: 12 }, + }, + ], }); }); @@ -340,9 +397,20 @@ describe('dirty-diff-computer', () => { '' ]); expect(dirtyDiff).to.be.deep.equal({ - removed: [11], - added: [{ start: 5, end: 5 }, { start: 9, end: 9 }], - modified: [], + changes: [ + { + previousRange: { start: 5, end: 5 }, + currentRange: { start: 5, end: 6 }, + }, + { + previousRange: { start: 8, end: 8 }, + currentRange: { start: 9, end: 10 }, + }, + { + previousRange: { start: 9, end: 10 }, + currentRange: { start: 12, end: 12 }, + }, + ], }); }); diff --git a/packages/scm/src/browser/dirty-diff/diff-computer.ts b/packages/scm/src/browser/dirty-diff/diff-computer.ts index 5662cb993a54e..38b95e591798b 100644 --- a/packages/scm/src/browser/dirty-diff/diff-computer.ts +++ b/packages/scm/src/browser/dirty-diff/diff-computer.ts @@ -16,6 +16,7 @@ import * as jsdiff from 'diff'; import { ContentLinesArrayLike } from './content-lines'; +import { Position, Range, uinteger } from '@theia/core/shared/vscode-languageserver-protocol'; export class DiffComputer { @@ -25,52 +26,52 @@ export class DiffComputer { } computeDirtyDiff(previous: ContentLinesArrayLike, current: ContentLinesArrayLike): DirtyDiff { - const added: LineRange[] = []; - const removed: number[] = []; - const modified: LineRange[] = []; - const changes = this.computeDiff(previous, current); - let lastLine = -1; - for (let i = 0; i < changes.length; i++) { - const change = changes[i]; - const next = changes[i + 1]; + const changes: Change[] = []; + const diffResult = this.computeDiff(previous, current); + let currentRevisionLine = -1; + let previousRevisionLine = -1; + for (let i = 0; i < diffResult.length; i++) { + const change = diffResult[i]; + const next = diffResult[i + 1]; if (change.added) { // case: addition - const start = lastLine + 1; - const end = lastLine + change.count!; - added.push({ start, end }); - lastLine = end; + changes.push({ previousRange: LineRange.createEmptyLineRange(previousRevisionLine + 1), currentRange: toLineRange(change) }); + currentRevisionLine += change.count!; } else if (change.removed && next && next.added) { const isFirstChange = i === 0; - const isLastChange = i === changes.length - 2; + const isLastChange = i === diffResult.length - 2; const isNextEmptyLine = next.value.length > 0 && current[next.value[0]].length === 0; const isPrevEmptyLine = change.value.length > 0 && previous[change.value[0]].length === 0; if (isFirstChange && isNextEmptyLine) { // special case: removing at the beginning - removed.push(0); + changes.push({ previousRange: toLineRange(change), currentRange: LineRange.createEmptyLineRange(0) }); + previousRevisionLine += change.count!; } else if (isFirstChange && isPrevEmptyLine) { // special case: adding at the beginning - const start = 0; - const end = next.count! - 1; - added.push({ start, end }); - lastLine = end; + changes.push({ previousRange: LineRange.createEmptyLineRange(0), currentRange: toLineRange(next) }); + currentRevisionLine += next.count!; } else if (isLastChange && isNextEmptyLine) { - removed.push(lastLine + 1 /* = empty line */); + changes.push({ previousRange: toLineRange(change), currentRange: LineRange.createEmptyLineRange(currentRevisionLine + 2) }); + previousRevisionLine += change.count!; } else { // default case is a modification - const start = lastLine + 1; - const end = lastLine + next.count!; - modified.push({ start, end }); - lastLine = end; + changes.push({ previousRange: toLineRange(change), currentRange: toLineRange(next) }); + currentRevisionLine += next.count!; + previousRevisionLine += change.count!; } i++; // consume next eagerly } else if (change.removed && !(next && next.added)) { - removed.push(Math.max(0, lastLine)); + // case: removal + changes.push({ previousRange: toLineRange(change), currentRange: LineRange.createEmptyLineRange(currentRevisionLine + 1) }); + previousRevisionLine += change.count!; } else { - lastLine += change.count!; + // case: unchanged region + currentRevisionLine += change.count!; + previousRevisionLine += change.count!; } } - return { added, removed, modified }; + return { changes }; } } @@ -101,6 +102,11 @@ function diffArrays(oldArr: ContentLinesArrayLike, newArr: ContentLinesArrayLike return arrayDiff.diff(oldArr as any, newArr as any) as any; } +function toLineRange({ value }: DiffResult): LineRange { + const [start, end] = value; + return LineRange.create(start, end + 1); +} + export interface DiffResult { value: [number, number]; count?: number; @@ -109,21 +115,63 @@ export interface DiffResult { } export interface DirtyDiff { - /** - * Lines added by comparison to previous revision. - */ - readonly added: LineRange[]; - /** - * Lines, after which lines were removed by comparison to previous revision. - */ - readonly removed: number[]; - /** - * Lines modified by comparison to previous revision. - */ - readonly modified: LineRange[]; + readonly changes: readonly Change[]; +} + +export interface Change { + readonly previousRange: LineRange; + readonly currentRange: LineRange; +} + +export namespace Change { + export function isAddition(change: Change): boolean { + return LineRange.isEmpty(change.previousRange); + } + export function isRemoval(change: Change): boolean { + return LineRange.isEmpty(change.currentRange); + } + export function isModification(change: Change): boolean { + return !isAddition(change) && !isRemoval(change); + } } export interface LineRange { - start: number; - end: number; + readonly start: number; + readonly end: number; +} + +export namespace LineRange { + export function create(start: number, end: number): LineRange { + if (start < 0 || end < 0 || start > end) { + throw new Error(`Invalid line range: { start: ${start}, end: ${end} }`); + } + return { start, end }; + } + export function createSingleLineRange(line: number): LineRange { + return create(line, line + 1); + } + export function createEmptyLineRange(line: number): LineRange { + return create(line, line); + } + export function isEmpty(range: LineRange): boolean { + return range.start === range.end; + } + export function getStartPosition(range: LineRange): Position { + if (isEmpty(range)) { + return getEndPosition(range); + } + return Position.create(range.start, 0); + } + export function getEndPosition(range: LineRange): Position { + if (range.end < 1) { + return Position.create(0, 0); + } + return Position.create(range.end - 1, uinteger.MAX_VALUE); + } + export function toRange(range: LineRange): Range { + return Range.create(getStartPosition(range), getEndPosition(range)); + } + export function getLineCount(range: LineRange): number { + return range.end - range.start; + } } diff --git a/packages/scm/src/browser/dirty-diff/dirty-diff-decorator.ts b/packages/scm/src/browser/dirty-diff/dirty-diff-decorator.ts index a6ff31676d959..cf17bee272f27 100644 --- a/packages/scm/src/browser/dirty-diff/dirty-diff-decorator.ts +++ b/packages/scm/src/browser/dirty-diff/dirty-diff-decorator.ts @@ -16,8 +16,6 @@ import { injectable } from '@theia/core/shared/inversify'; import { - Range, - Position, EditorDecoration, EditorDecorationOptions, OverviewRulerLane, @@ -25,7 +23,8 @@ import { TextEditor, MinimapPosition } from '@theia/editor/lib/browser'; -import { DirtyDiff, LineRange } from './diff-computer'; +import { DirtyDiff, LineRange, Change } from './diff-computer'; +import { URI } from '@theia/core'; export enum DirtyDiffDecorationType { AddedLine = 'dirty-diff-added-line', @@ -84,24 +83,32 @@ const ModifiedLineDecoration = { isWholeLine: true }; +function getEditorDecorationOptions(change: Change): EditorDecorationOptions { + if (Change.isAddition(change)) { + return AddedLineDecoration; + } + if (Change.isRemoval(change)) { + return RemovedLineDecoration; + } + return ModifiedLineDecoration; +} + export interface DirtyDiffUpdate extends DirtyDiff { readonly editor: TextEditor; + readonly previousRevisionUri?: URI; } @injectable() export class DirtyDiffDecorator extends EditorDecorator { applyDecorations(update: DirtyDiffUpdate): void { - const modifications = update.modified.map(range => this.toDeltaDecoration(range, ModifiedLineDecoration)); - const additions = update.added.map(range => this.toDeltaDecoration(range, AddedLineDecoration)); - const removals = update.removed.map(line => this.toDeltaDecoration(line, RemovedLineDecoration)); - const decorations = [...modifications, ...additions, ...removals]; + const decorations = update.changes.map(change => this.toDeltaDecoration(change)); this.setDecorations(update.editor, decorations); } - protected toDeltaDecoration(from: LineRange | number, options: EditorDecorationOptions): EditorDecoration { - const [start, end] = (typeof from === 'number') ? [from, from] : [from.start, from.end]; - const range = Range.create(Position.create(start, 0), Position.create(end, 0)); + protected toDeltaDecoration(change: Change): EditorDecoration { + const range = LineRange.toRange(change.currentRange); + const options = getEditorDecorationOptions(change); return { range, options }; } } diff --git a/packages/scm/src/browser/dirty-diff/dirty-diff-module.ts b/packages/scm/src/browser/dirty-diff/dirty-diff-module.ts index 1982324afa773..3b2117f0f58f2 100644 --- a/packages/scm/src/browser/dirty-diff/dirty-diff-module.ts +++ b/packages/scm/src/browser/dirty-diff/dirty-diff-module.ts @@ -16,9 +16,18 @@ import { interfaces } from '@theia/core/shared/inversify'; import { DirtyDiffDecorator } from './dirty-diff-decorator'; +import { DirtyDiffNavigator } from './dirty-diff-navigator'; +import { DirtyDiffWidget, DirtyDiffWidgetFactory, DirtyDiffWidgetProps } from './dirty-diff-widget'; import '../../../src/browser/style/dirty-diff.css'; export function bindDirtyDiff(bind: interfaces.Bind): void { bind(DirtyDiffDecorator).toSelf().inSingletonScope(); + bind(DirtyDiffNavigator).toSelf().inSingletonScope(); + bind(DirtyDiffWidgetFactory).toFactory(({ container }) => props => { + const child = container.createChild(); + child.bind(DirtyDiffWidgetProps).toConstantValue(props); + child.bind(DirtyDiffWidget).toSelf(); + return child.get(DirtyDiffWidget); + }); } diff --git a/packages/scm/src/browser/dirty-diff/dirty-diff-navigator.ts b/packages/scm/src/browser/dirty-diff/dirty-diff-navigator.ts new file mode 100644 index 0000000000000..d765797337617 --- /dev/null +++ b/packages/scm/src/browser/dirty-diff/dirty-diff-navigator.ts @@ -0,0 +1,288 @@ +// ***************************************************************************** +// Copyright (C) 2023 1C-Soft LLC 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 { Disposable, DisposableCollection, URI } from '@theia/core'; +import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { EditorManager, EditorMouseEvent, MouseTargetType, TextEditor } from '@theia/editor/lib/browser'; +import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; +import { Change, LineRange } from './diff-computer'; +import { DirtyDiffUpdate } from './dirty-diff-decorator'; +import { DirtyDiffWidget, DirtyDiffWidgetFactory } from './dirty-diff-widget'; + +@injectable() +export class DirtyDiffNavigator { + + protected readonly controllers = new Map(); + + @inject(ContextKeyService) + protected readonly contextKeyService: ContextKeyService; + + @inject(EditorManager) + protected readonly editorManager: EditorManager; + + @inject(DirtyDiffWidgetFactory) + protected readonly widgetFactory: DirtyDiffWidgetFactory; + + @postConstruct() + protected init(): void { + const dirtyDiffVisible: ContextKey = this.contextKeyService.createKey('dirtyDiffVisible', false); + this.editorManager.onActiveEditorChanged(editorWidget => { + dirtyDiffVisible.set(editorWidget && this.controllers.get(editorWidget.editor)?.isShowingChange()); + }); + this.editorManager.onCreated(editorWidget => { + const { editor } = editorWidget; + if (editor.uri.scheme !== 'file') { + return; + } + const controller = this.createController(editor); + controller.widgetFactory = props => { + const widget = this.widgetFactory(props); + if (widget.editor === this.editorManager.activeEditor?.editor) { + dirtyDiffVisible.set(true); + } + widget.onDidClose(() => { + if (widget.editor === this.editorManager.activeEditor?.editor) { + dirtyDiffVisible.set(false); + } + }); + return widget; + }; + this.controllers.set(editor, controller); + editorWidget.disposed.connect(() => { + this.controllers.delete(editor); + controller.dispose(); + }); + }); + } + + handleDirtyDiffUpdate(update: DirtyDiffUpdate): void { + const controller = this.controllers.get(update.editor); + controller?.handleDirtyDiffUpdate(update); + } + + canNavigate(): boolean { + return !!this.activeController?.canNavigate(); + } + + gotoNextChange(): void { + this.activeController?.gotoNextChange(); + } + + gotoPreviousChange(): void { + this.activeController?.gotoPreviousChange(); + } + + canShowChange(): boolean { + return !!this.activeController?.canShowChange(); + } + + showNextChange(): void { + this.activeController?.showNextChange(); + } + + showPreviousChange(): void { + this.activeController?.showPreviousChange(); + } + + isShowingChange(): boolean { + return !!this.activeController?.isShowingChange(); + } + + closeChangePeekView(): void { + this.activeController?.closeWidget(); + } + + protected get activeController(): DirtyDiffController | undefined { + const editor = this.editorManager.activeEditor?.editor; + return editor && this.controllers.get(editor); + } + + protected createController(editor: TextEditor): DirtyDiffController { + return new DirtyDiffController(editor); + } +} + +export class DirtyDiffController implements Disposable { + + protected readonly toDispose = new DisposableCollection(); + + widgetFactory?: DirtyDiffWidgetFactory; + protected widget?: DirtyDiffWidget; + protected dirtyDiff?: DirtyDiffUpdate; + + constructor(protected readonly editor: TextEditor) { + editor.onMouseDown(this.handleEditorMouseDown, this, this.toDispose); + } + + dispose(): void { + this.closeWidget(); + this.toDispose.dispose(); + } + + handleDirtyDiffUpdate(dirtyDiff: DirtyDiffUpdate): void { + if (dirtyDiff.editor === this.editor) { + this.closeWidget(); + this.dirtyDiff = dirtyDiff; + } + } + + canNavigate(): boolean { + return !!this.changes?.length; + } + + gotoNextChange(): void { + const { editor } = this; + const index = this.findNextClosestChange(editor.cursor.line, false); + const change = this.changes?.[index]; + if (change) { + const position = LineRange.getStartPosition(change.currentRange); + editor.cursor = position; + editor.revealPosition(position, { vertical: 'auto' }); + } + } + + gotoPreviousChange(): void { + const { editor } = this; + const index = this.findPreviousClosestChange(editor.cursor.line, false); + const change = this.changes?.[index]; + if (change) { + const position = LineRange.getStartPosition(change.currentRange); + editor.cursor = position; + editor.revealPosition(position, { vertical: 'auto' }); + } + } + + canShowChange(): boolean { + return !!(this.widget || this.widgetFactory && this.editor instanceof MonacoEditor && this.changes?.length && this.previousRevisionUri); + } + + showNextChange(): void { + if (this.widget) { + this.widget.showNextChange(); + } else { + (this.widget = this.createWidget())?.showChange( + this.findNextClosestChange(this.editor.cursor.line, true)); + } + } + + showPreviousChange(): void { + if (this.widget) { + this.widget.showPreviousChange(); + } else { + (this.widget = this.createWidget())?.showChange( + this.findPreviousClosestChange(this.editor.cursor.line, true)); + } + } + + isShowingChange(): boolean { + return !!this.widget; + } + + closeWidget(): void { + if (this.widget) { + this.widget.dispose(); + this.widget = undefined; + } + } + + protected get changes(): readonly Change[] | undefined { + return this.dirtyDiff?.changes; + } + + protected get previousRevisionUri(): URI | undefined { + return this.dirtyDiff?.previousRevisionUri; + } + + protected createWidget(): DirtyDiffWidget | undefined { + const { widgetFactory, editor, changes, previousRevisionUri } = this; + if (widgetFactory && editor instanceof MonacoEditor && changes?.length && previousRevisionUri) { + const widget = widgetFactory({ editor, previousRevisionUri, changes }); + widget.onDidClose(() => { + this.widget = undefined; + }); + return widget; + } + } + + protected findNextClosestChange(line: number, inclusive: boolean): number { + const length = this.changes?.length; + if (!length) { + return -1; + } + for (let i = 0; i < length; i++) { + const { currentRange } = this.changes![i]; + + if (inclusive) { + if (LineRange.getEndPosition(currentRange).line >= line) { + return i; + } + } else { + if (LineRange.getStartPosition(currentRange).line > line) { + return i; + } + } + } + return 0; + } + + protected findPreviousClosestChange(line: number, inclusive: boolean): number { + const length = this.changes?.length; + if (!length) { + return -1; + } + for (let i = length - 1; i >= 0; i--) { + const { currentRange } = this.changes![i]; + + if (inclusive) { + if (LineRange.getStartPosition(currentRange).line <= line) { + return i; + } + } else { + if (LineRange.getEndPosition(currentRange).line < line) { + return i; + } + } + } + return length - 1; + } + + protected handleEditorMouseDown({ event, target }: EditorMouseEvent): void { + if (event.button !== 0) { + return; + } + const { range, type, element } = target; + if (!range || type !== MouseTargetType.GUTTER_LINE_DECORATIONS || !element || element.className.indexOf('dirty-diff-glyph') < 0) { + return; + } + const gutterOffsetX = target.detail.offsetX - (element as HTMLElement).offsetLeft; + if (gutterOffsetX < -3 || gutterOffsetX > 3) { // dirty diff decoration on hover is 6px wide + return; // to avoid colliding with folding + } + const index = this.findNextClosestChange(range.start.line, true); + if (index < 0) { + return; + } + if (index === this.widget?.currentChangeIndex) { + this.closeWidget(); + return; + } + if (!this.widget) { + this.widget = this.createWidget(); + } + this.widget?.showChange(index); + } +} diff --git a/packages/scm/src/browser/dirty-diff/dirty-diff-widget.ts b/packages/scm/src/browser/dirty-diff/dirty-diff-widget.ts new file mode 100644 index 0000000000000..4fe5d5b5e58e9 --- /dev/null +++ b/packages/scm/src/browser/dirty-diff/dirty-diff-widget.ts @@ -0,0 +1,364 @@ +// ***************************************************************************** +// Copyright (C) 2023 1C-Soft LLC 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 { Position, Range } from '@theia/core/shared/vscode-languageserver-protocol'; +import { ActionMenuNode, Disposable, Emitter, Event, MenuCommandExecutor, MenuModelRegistry, MenuPath, URI, nls } from '@theia/core'; +import { codicon } from '@theia/core/lib/browser'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; +import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor'; +import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; +import { MonacoEditorPeekViewWidget, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground } + from '@theia/monaco/lib/browser/monaco-editor-peek-view-widget'; +import { Change, LineRange } from './diff-computer'; +import { ScmColors } from '../scm-colors'; +import * as monaco from '@theia/monaco-editor-core'; + +export const SCM_CHANGE_TITLE_MENU: MenuPath = ['scm-change-title-menu']; +/** Reserved for plugin contributions, corresponds to contribution point 'scm/change/title'. */ +export const PLUGIN_SCM_CHANGE_TITLE_MENU: MenuPath = ['plugin-scm-change-title-menu']; + +export const DirtyDiffWidgetProps = Symbol('DirtyDiffWidgetProps'); +export interface DirtyDiffWidgetProps { + readonly editor: MonacoEditor; + readonly previousRevisionUri: URI; + readonly changes: readonly Change[]; +} + +export const DirtyDiffWidgetFactory = Symbol('DirtyDiffWidgetFactory'); +export type DirtyDiffWidgetFactory = (props: DirtyDiffWidgetProps) => DirtyDiffWidget; + +@injectable() +export class DirtyDiffWidget implements Disposable { + + private readonly onDidCloseEmitter = new Emitter(); + readonly onDidClose: Event = this.onDidCloseEmitter.event; + protected index: number = -1; + private peekView?: DirtyDiffPeekView; + private diffEditorPromise?: Promise; + + constructor( + @inject(DirtyDiffWidgetProps) protected readonly props: DirtyDiffWidgetProps, + @inject(MonacoEditorProvider) readonly editorProvider: MonacoEditorProvider, + @inject(ContextKeyService) readonly contextKeyService: ContextKeyService, + @inject(MenuModelRegistry) readonly menuModelRegistry: MenuModelRegistry, + @inject(MenuCommandExecutor) readonly menuCommandExecutor: MenuCommandExecutor + ) { } + + @postConstruct() + create(): void { + this.peekView = new DirtyDiffPeekView(this); + this.peekView.onDidClose(e => this.onDidCloseEmitter.fire(e)); + this.diffEditorPromise = this.peekView.create(); + } + + get editor(): MonacoEditor { + return this.props.editor; + } + + get uri(): URI { + return this.editor.uri; + } + + get previousRevisionUri(): URI { + return this.props.previousRevisionUri; + } + + get changes(): readonly Change[] { + return this.props.changes; + } + + get currentChange(): Change | undefined { + return this.changes[this.index]; + } + + get currentChangeIndex(): number { + return this.index; + } + + showChange(index: number): void { + this.checkCreated(); + if (index >= 0 && index < this.changes.length) { + this.index = index; + this.showCurrentChange(); + } + } + + showNextChange(): void { + this.checkCreated(); + const index = this.index; + const length = this.changes.length; + if (length > 0 && (index < 0 || length > 1)) { + this.index = index < 0 ? 0 : cycle(index, 1, length); + this.showCurrentChange(); + } + } + + showPreviousChange(): void { + this.checkCreated(); + const index = this.index; + const length = this.changes.length; + if (length > 0 && (index < 0 || length > 1)) { + this.index = index < 0 ? length - 1 : cycle(index, -1, length); + this.showCurrentChange(); + } + } + + async getContentWithSelectedChanges(predicate: (change: Change, index: number, changes: readonly Change[]) => boolean): Promise { + this.checkCreated(); + const changes = this.changes.filter(predicate); + const { diffEditor } = await this.diffEditorPromise!; + const diffEditorModel = diffEditor.getModel()!; + return applyChanges(changes, diffEditorModel.original, diffEditorModel.modified); + } + + dispose(): void { + this.peekView?.dispose(); + this.onDidCloseEmitter.dispose(); + } + + protected showCurrentChange(): void { + this.peekView!.setTitle(this.computePrimaryHeading(), this.computeSecondaryHeading()); + const { previousRange, currentRange } = this.changes[this.index]; + this.peekView!.show(Position.create(LineRange.getEndPosition(currentRange).line, 0), + this.computeHeightInLines()); + this.diffEditorPromise!.then(({ diffEditor }) => { + let startLine = LineRange.getStartPosition(currentRange).line; + let endLine = LineRange.getEndPosition(currentRange).line; + if (LineRange.isEmpty(currentRange)) { // the change is a removal + ++endLine; + } else if (!LineRange.isEmpty(previousRange)) { // the change is a modification + --startLine; + ++endLine; + } + diffEditor.revealLinesInCenter(startLine + 1, endLine + 1, // monaco line numbers are 1-based + monaco.editor.ScrollType.Immediate); + }); + this.editor.focus(); + } + + protected computePrimaryHeading(): string { + return this.uri.path.base; + } + + protected computeSecondaryHeading(): string { + const index = this.index + 1; + const length = this.changes.length; + return length > 1 ? nls.localizeByDefault('{0} of {1} changes', index, length) : + nls.localizeByDefault('{0} of {1} change', index, length); + } + + protected computeHeightInLines(): number { + const editor = this.editor.getControl(); + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); + const editorHeight = editor.getLayoutInfo().height; + const editorHeightInLines = Math.floor(editorHeight / lineHeight); + + const { previousRange, currentRange } = this.changes[this.index]; + const changeHeightInLines = LineRange.getLineCount(currentRange) + LineRange.getLineCount(previousRange); + + return Math.min(changeHeightInLines + /* padding */ 8, Math.floor(editorHeightInLines / 3)); + } + + protected checkCreated(): void { + if (!this.peekView) { + throw new Error('create() method needs to be called first.'); + } + } +} + +function cycle(index: number, offset: -1 | 1, length: number): number { + return (index + offset + length) % length; +} + +// adapted from https://github.com/microsoft/vscode/blob/823d54f86ee13eb357bc6e8e562e89d793f3c43b/extensions/git/src/staging.ts +function applyChanges(changes: readonly Change[], original: monaco.editor.ITextModel, modified: monaco.editor.ITextModel): string { + const result: string[] = []; + let currentLine = 1; + + for (const change of changes) { + const { previousRange, currentRange } = change; + + const isInsertion = LineRange.isEmpty(previousRange); + const isDeletion = LineRange.isEmpty(currentRange); + + const convert = (range: LineRange): [number, number] => { + let startLineNumber; + let endLineNumber; + if (!LineRange.isEmpty(range)) { + startLineNumber = range.start + 1; + endLineNumber = range.end; + } else { + startLineNumber = range.start; + endLineNumber = 0; + } + return [startLineNumber, endLineNumber]; + }; + + const [originalStartLineNumber, originalEndLineNumber] = convert(previousRange); + const [modifiedStartLineNumber, modifiedEndLineNumber] = convert(currentRange); + + let toLine = isInsertion ? originalStartLineNumber + 1 : originalStartLineNumber; + let toCharacter = 1; + + // if this is a deletion at the very end of the document, + // we need to account for a newline at the end of the last line, + // which may have been deleted + if (isDeletion && originalEndLineNumber === original.getLineCount()) { + toLine--; + toCharacter = original.getLineMaxColumn(toLine); + } + + result.push(original.getValueInRange(new monaco.Range(currentLine, 1, toLine, toCharacter))); + + if (!isDeletion) { + let fromLine = modifiedStartLineNumber; + let fromCharacter = 1; + + // if this is an insertion at the very end of the document, + // we must start the next range after the last character of the previous line, + // in order to take the correct eol + if (isInsertion && originalStartLineNumber === original.getLineCount()) { + fromLine--; + fromCharacter = modified.getLineMaxColumn(fromLine); + } + + result.push(modified.getValueInRange(new monaco.Range(fromLine, fromCharacter, modifiedEndLineNumber + 1, 1))); + } + + currentLine = isInsertion ? originalStartLineNumber + 1 : originalEndLineNumber + 1; + } + + result.push(original.getValueInRange(new monaco.Range(currentLine, 1, original.getLineCount() + 1, 1))); + + return result.join(''); +} + +class DirtyDiffPeekView extends MonacoEditorPeekViewWidget { + + private diffEditorPromise?: Promise; + private height?: number; + + constructor(readonly widget: DirtyDiffWidget) { + super(widget.editor, { isResizeable: true, showArrow: true, frameWidth: 1, keepEditorSelection: true, className: 'dirty-diff' }); + } + + override async create(): Promise { + try { + super.create(); + const diffEditor = await this.diffEditorPromise!; + return new Promise(resolve => { + // setTimeout is needed here because the non-side-by-side diff editor might still not have created the view zones; + // otherwise, the first change shown might not be properly revealed in the diff editor. + // see also https://github.com/microsoft/vscode/blob/b30900b56c4b3ca6c65d7ab92032651f4cb23f15/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts#L248 + const disposable = diffEditor.diffEditor.onDidUpdateDiff(() => setTimeout(() => { + resolve(diffEditor); + disposable.dispose(); + })); + }); + } catch (e) { + this.dispose(); + throw e; + } + } + + override show(rangeOrPos: Range | Position, heightInLines: number): void { + const borderColor = this.getBorderColor(); + this.style({ + arrowColor: borderColor, + frameColor: borderColor, + headerBackgroundColor: peekViewTitleBackground, + primaryHeadingColor: peekViewTitleForeground, + secondaryHeadingColor: peekViewTitleInfoForeground + }); + this.updateActions(); + super.show(rangeOrPos, heightInLines); + } + + private getBorderColor(): string { + const { currentChange } = this.widget; + if (!currentChange) { + return peekViewBorder; + } + if (Change.isAddition(currentChange)) { + return ScmColors.editorGutterAddedBackground; + } else if (Change.isRemoval(currentChange)) { + return ScmColors.editorGutterDeletedBackground; + } else { + return ScmColors.editorGutterModifiedBackground; + } + } + + private updateActions(): void { + this.clearActions(); + const { contextKeyService, menuModelRegistry, menuCommandExecutor } = this.widget; + contextKeyService.with({ originalResourceScheme: this.widget.previousRevisionUri.scheme }, () => { + for (const menuPath of [SCM_CHANGE_TITLE_MENU, PLUGIN_SCM_CHANGE_TITLE_MENU]) { + const menu = menuModelRegistry.getMenu(menuPath); + for (const item of menu.children) { + if (item instanceof ActionMenuNode) { + const { command, id, label, icon, when } = item; + if (icon && menuCommandExecutor.isVisible(menuPath, command, this.widget) && (!when || contextKeyService.match(when))) { + this.addAction(id, label, icon, menuCommandExecutor.isEnabled(menuPath, command, this.widget), () => { + menuCommandExecutor.executeCommand(menuPath, command, this.widget); + }); + } + } + } + } + }); + this.addAction('dirtydiff.next', nls.localizeByDefault('Show Next Change'), codicon('arrow-down'), true, + () => this.widget.showNextChange()); + this.addAction('dirtydiff.previous', nls.localizeByDefault('Show Previous Change'), codicon('arrow-up'), true, + () => this.widget.showPreviousChange()); + this.addAction('peekview.close', nls.localizeByDefault('Close'), codicon('close'), true, + () => this.dispose()); + } + + protected override fillHead(container: HTMLElement): void { + super.fillHead(container, true); + } + + protected override fillBody(container: HTMLElement): void { + this.diffEditorPromise = this.widget.editorProvider.createEmbeddedDiffEditor(this.editor, container, this.widget.previousRevisionUri).then(diffEditor => { + this.toDispose.push(diffEditor); + return diffEditor; + }); + } + + protected override doLayoutBody(height: number, width: number): void { + super.doLayoutBody(height, width); + this.layout(height, width); + this.height = height; + } + + protected override onWidth(width: number): void { + super.onWidth(width); + const { height } = this; + if (height !== undefined) { + this.layout(height, width); + } + } + + private layout(height: number, width: number): void { + this.diffEditorPromise?.then(({ diffEditor }) => diffEditor.layout({ height, width })); + } + + protected override doRevealRange(range: Range): void { + this.editor.revealPosition(Position.create(range.end.line, 0), { vertical: 'centerIfOutsideViewport' }); + } +} diff --git a/packages/scm/src/browser/scm-colors.ts b/packages/scm/src/browser/scm-colors.ts new file mode 100644 index 0000000000000..853d218e679d8 --- /dev/null +++ b/packages/scm/src/browser/scm-colors.ts @@ -0,0 +1,21 @@ +// ***************************************************************************** +// Copyright (C) 2019 Red Hat, Inc. 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 +// ***************************************************************************** + +export namespace ScmColors { + export const editorGutterModifiedBackground = 'editorGutter.modifiedBackground'; + export const editorGutterAddedBackground = 'editorGutter.addedBackground'; + export const editorGutterDeletedBackground = 'editorGutter.deletedBackground'; +} diff --git a/packages/scm/src/browser/scm-contribution.ts b/packages/scm/src/browser/scm-contribution.ts index 032079923632a..f896363f313de 100644 --- a/packages/scm/src/browser/scm-contribution.ts +++ b/packages/scm/src/browser/scm-contribution.ts @@ -29,7 +29,7 @@ import { CssStyleCollector } from '@theia/core/lib/browser'; import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { CommandRegistry, Command, Disposable, DisposableCollection, CommandService } from '@theia/core/lib/common'; +import { CommandRegistry, Command, Disposable, DisposableCollection, CommandService, MenuModelRegistry } from '@theia/core/lib/common'; import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service'; import { ScmService } from './scm-service'; import { ScmWidget } from '../browser/scm-widget'; @@ -38,10 +38,13 @@ import { ScmQuickOpenService } from './scm-quick-open-service'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { Color } from '@theia/core/lib/common/color'; +import { ScmColors } from './scm-colors'; import { ScmCommand } from './scm-provider'; import { ScmDecorationsService } from '../browser/decorations/scm-decorations-service'; import { nls } from '@theia/core/lib/common/nls'; import { isHighContrast } from '@theia/core/lib/common/theme'; +import { EditorMainMenu } from '@theia/editor/lib/browser'; +import { DirtyDiffNavigator } from './dirty-diff/dirty-diff-navigator'; export const SCM_WIDGET_FACTORY_ID = ScmWidget.ID; export const SCM_VIEW_CONTAINER_ID = 'scm-view-container'; @@ -51,6 +54,10 @@ export const SCM_VIEW_CONTAINER_TITLE_OPTIONS: ViewContainerTitleOptions = { closeable: true }; +export namespace ScmMenus { + export const CHANGES_GROUP = [...EditorMainMenu.GO, '6_changes_group']; +} + export namespace SCM_COMMANDS { export const CHANGE_REPOSITORY = { id: 'scm.change.repository', @@ -85,13 +92,36 @@ export namespace SCM_COMMANDS { label: nls.localizeByDefault('Collapse All'), originalLabel: 'Collapse All' }; + export const GOTO_NEXT_CHANGE = Command.toDefaultLocalizedCommand({ + id: 'workbench.action.editor.nextChange', + category: 'Source Control', + label: 'Go to Next Change' + }); + export const GOTO_PREVIOUS_CHANGE = Command.toDefaultLocalizedCommand({ + id: 'workbench.action.editor.previousChange', + category: 'Source Control', + label: 'Go to Previous Change' + }); + export const SHOW_NEXT_CHANGE = Command.toDefaultLocalizedCommand({ + id: 'editor.action.dirtydiff.next', + category: 'Source Control', + label: 'Show Next Change' + }); + export const SHOW_PREVIOUS_CHANGE = Command.toDefaultLocalizedCommand({ + id: 'editor.action.dirtydiff.previous', + category: 'Source Control', + label: 'Show Previous Change' + }); + export const CLOSE_CHANGE_PEEK_VIEW = { + id: 'editor.action.dirtydiff.close', + category: nls.localizeByDefault('Source Control'), + originalCategory: 'Source Control', + label: nls.localize('theia/scm/dirtyDiff/close', 'Close Change Peek View'), + originalLabel: 'Close Change Peek View' + }; } -export namespace ScmColors { - export const editorGutterModifiedBackground = 'editorGutter.modifiedBackground'; - export const editorGutterAddedBackground = 'editorGutter.addedBackground'; - export const editorGutterDeletedBackground = 'editorGutter.deletedBackground'; -} +export { ScmColors }; @injectable() export class ScmContribution extends AbstractViewContribution implements @@ -108,6 +138,7 @@ export class ScmContribution extends AbstractViewContribution impleme @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(ContextKeyService) protected readonly contextKeys: ContextKeyService; @inject(ScmDecorationsService) protected readonly scmDecorationsService: ScmDecorationsService; + @inject(DirtyDiffNavigator) protected readonly dirtyDiffNavigator: DirtyDiffNavigator; protected scmFocus: ContextKey; @@ -144,6 +175,8 @@ export class ScmContribution extends AbstractViewContribution impleme this.updateContextKeys(); this.shell.onDidChangeCurrentWidget(() => this.updateContextKeys()); + + this.scmDecorationsService.onDirtyDiffUpdate(update => this.dirtyDiffNavigator.handleDirtyDiffUpdate(update)); } protected updateContextKeys(): void { @@ -160,6 +193,39 @@ export class ScmContribution extends AbstractViewContribution impleme execute: () => this.acceptInput(), isEnabled: () => !!this.scmFocus.get() && !!this.acceptInputCommand() }); + + // Note that commands for dirty diff navigation need to be always available. + // This is consistent with behavior in VS Code, and also with other similar commands (such as `Next Problem/Previous Problem`) in Theia. + // See https://github.com/eclipse-theia/theia/pull/13104#discussion_r1497316614 for a detailed discussion. + commandRegistry.registerCommand(SCM_COMMANDS.GOTO_NEXT_CHANGE, { + execute: () => this.dirtyDiffNavigator.gotoNextChange() + }); + commandRegistry.registerCommand(SCM_COMMANDS.GOTO_PREVIOUS_CHANGE, { + execute: () => this.dirtyDiffNavigator.gotoPreviousChange() + }); + commandRegistry.registerCommand(SCM_COMMANDS.SHOW_NEXT_CHANGE, { + execute: () => this.dirtyDiffNavigator.showNextChange() + }); + commandRegistry.registerCommand(SCM_COMMANDS.SHOW_PREVIOUS_CHANGE, { + execute: () => this.dirtyDiffNavigator.showPreviousChange() + }); + commandRegistry.registerCommand(SCM_COMMANDS.CLOSE_CHANGE_PEEK_VIEW, { + execute: () => this.dirtyDiffNavigator.closeChangePeekView() + }); + } + + override registerMenus(menus: MenuModelRegistry): void { + super.registerMenus(menus); + menus.registerMenuAction(ScmMenus.CHANGES_GROUP, { + commandId: SCM_COMMANDS.SHOW_NEXT_CHANGE.id, + label: nls.localizeByDefault('Next Change'), + order: '1' + }); + menus.registerMenuAction(ScmMenus.CHANGES_GROUP, { + commandId: SCM_COMMANDS.SHOW_PREVIOUS_CHANGE.id, + label: nls.localizeByDefault('Previous Change'), + order: '2' + }); } registerToolbarItems(registry: TabBarToolbarRegistry): void { @@ -219,6 +285,31 @@ export class ScmContribution extends AbstractViewContribution impleme keybinding: 'ctrlcmd+enter', when: 'scmFocus' }); + keybindings.registerKeybinding({ + command: SCM_COMMANDS.GOTO_NEXT_CHANGE.id, + keybinding: 'alt+f5', + when: 'editorTextFocus' + }); + keybindings.registerKeybinding({ + command: SCM_COMMANDS.GOTO_PREVIOUS_CHANGE.id, + keybinding: 'shift+alt+f5', + when: 'editorTextFocus' + }); + keybindings.registerKeybinding({ + command: SCM_COMMANDS.SHOW_NEXT_CHANGE.id, + keybinding: 'alt+f3', + when: 'editorTextFocus' + }); + keybindings.registerKeybinding({ + command: SCM_COMMANDS.SHOW_PREVIOUS_CHANGE.id, + keybinding: 'shift+alt+f3', + when: 'editorTextFocus' + }); + keybindings.registerKeybinding({ + command: SCM_COMMANDS.CLOSE_CHANGE_PEEK_VIEW.id, + keybinding: 'esc', + when: 'dirtyDiffVisible' + }); } protected async acceptInput(): Promise { diff --git a/packages/scm/src/browser/scm-tree-widget.tsx b/packages/scm/src/browser/scm-tree-widget.tsx index 105956cf85d4b..3dd0b346c9bc9 100644 --- a/packages/scm/src/browser/scm-tree-widget.tsx +++ b/packages/scm/src/browser/scm-tree-widget.tsx @@ -605,7 +605,7 @@ export class ScmResourceComponent extends ScmElement protected readonly contextMenuPath = ScmTreeWidget.RESOURCE_CONTEXT_MENU; protected get contextMenuArgs(): any[] { - if (!this.props.model.selectedNodes.some(node => ScmFileChangeNode.is(node) && node.sourceUri === this.props.sourceUri)) { + if (!this.props.model.selectedNodes.some(node => ScmFileChangeNode.is(node) && node === this.props.treeNode)) { // Clicked node is not in selection, so ignore selection and action on just clicked node return this.singleNodeArgs; } else { diff --git a/packages/scm/src/browser/style/dirty-diff-decorator.css b/packages/scm/src/browser/style/dirty-diff-decorator.css index 1b630c3ba07ac..f5f8beeb8c08e 100644 --- a/packages/scm/src/browser/style/dirty-diff-decorator.css +++ b/packages/scm/src/browser/style/dirty-diff-decorator.css @@ -19,6 +19,7 @@ border-top: 4px solid transparent; border-bottom: 4px solid transparent; transition: border-top-width 80ms linear, border-bottom-width 80ms linear, bottom 80ms linear; + pointer-events: none; } .dirty-diff-glyph:before { @@ -41,7 +42,7 @@ position: absolute; content: ''; height: 100%; - width: 9px; + width: 6px; left: -6px; } diff --git a/packages/scm/tsconfig.json b/packages/scm/tsconfig.json index 41c8ab00ce84c..8f53c0fe2dd53 100644 --- a/packages/scm/tsconfig.json +++ b/packages/scm/tsconfig.json @@ -17,6 +17,9 @@ }, { "path": "../filesystem" + }, + { + "path": "../monaco" } ] } diff --git a/packages/search-in-workspace/package.json b/packages/search-in-workspace/package.json index 52a9a642a69b8..2b8e007c68d19 100644 --- a/packages/search-in-workspace/package.json +++ b/packages/search-in-workspace/package.json @@ -1,17 +1,18 @@ { "name": "@theia/search-in-workspace", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Search in workspace", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/navigator": "1.44.0", - "@theia/process": "1.44.0", - "@theia/workspace": "1.44.0", + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/navigator": "1.54.0", + "@theia/process": "1.54.0", + "@theia/workspace": "1.54.0", "@vscode/ripgrep": "^1.14.2", "minimatch": "^5.1.0", - "react-autosize-textarea": "^7.0.0" + "react-autosize-textarea": "^7.0.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -47,6 +48,6 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" } } diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts index 754c2020e434f..19f041953eb22 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts @@ -57,6 +57,16 @@ export namespace SearchInWorkspaceCommands { category: SEARCH_CATEGORY, label: 'Find in Folder...' }); + export const FOCUS_NEXT_RESULT = Command.toDefaultLocalizedCommand({ + id: 'search.action.focusNextSearchResult', + category: SEARCH_CATEGORY, + label: 'Focus Next Search Result' + }); + export const FOCUS_PREV_RESULT = Command.toDefaultLocalizedCommand({ + id: 'search.action.focusPreviousSearchResult', + category: SEARCH_CATEGORY, + label: 'Focus Previous Search Result' + }); export const REFRESH_RESULTS = Command.toDefaultLocalizedCommand({ id: 'search-in-workspace.refresh', category: SEARCH_CATEGORY, @@ -169,6 +179,22 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut } }); + commands.registerCommand(SearchInWorkspaceCommands.FOCUS_NEXT_RESULT, { + isEnabled: () => this.withWidget(undefined, widget => widget.hasResultList()), + execute: async () => { + const widget = await this.openView({ reveal: true }); + widget.resultTreeWidget.selectNextResult(); + } + }); + + commands.registerCommand(SearchInWorkspaceCommands.FOCUS_PREV_RESULT, { + isEnabled: () => this.withWidget(undefined, widget => widget.hasResultList()), + execute: async () => { + const widget = await this.openView({ reveal: true }); + widget.resultTreeWidget.selectPreviousResult(); + } + }); + commands.registerCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER, this.newMultiUriAwareCommandHandler({ execute: async uris => { const resources: string[] = []; @@ -343,6 +369,16 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut keybinding: 'shift+alt+f', when: 'explorerResourceIsFolder' }); + keybindings.registerKeybinding({ + command: SearchInWorkspaceCommands.FOCUS_NEXT_RESULT.id, + keybinding: 'f4', + when: 'hasSearchResult' + }); + keybindings.registerKeybinding({ + command: SearchInWorkspaceCommands.FOCUS_PREV_RESULT.id, + keybinding: 'shift+f4', + when: 'hasSearchResult' + }); keybindings.registerKeybinding({ command: SearchInWorkspaceCommands.DISMISS_RESULT.id, keybinding: isOSX ? 'cmd+backspace' : 'del', diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx index 2b6922ba3f7c7..1abf85b4a678f 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx @@ -281,6 +281,79 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { return true; } + selectNextResult(): void { + if (!this.model.getFocusedNode()) { + return this.selectFirstResult(); + } + let foundNextResult = false; + while (!foundNextResult) { + const nextNode = this.model.getNextNode(); + if (!nextNode) { + return this.selectFirstResult(); + } else if (SearchInWorkspaceResultLineNode.is(nextNode)) { + foundNextResult = true; + this.selectExpandOpenResultNode(nextNode); + } else { + this.model.selectNext(); + } + } + } + + selectPreviousResult(): void { + if (!this.model.getFocusedNode()) { + return this.selectLastResult(); + } + let foundSelectedNode = false; + while (!foundSelectedNode) { + const prevNode = this.model.getPrevNode(); + if (!prevNode) { + return this.selectLastResult(); + } else if (SearchInWorkspaceResultLineNode.is(prevNode)) { + foundSelectedNode = true; + this.selectExpandOpenResultNode(prevNode); + } else if (prevNode.id === 'ResultTree') { + return this.selectLastResult(); + } else { + this.model.selectPrev(); + } + } + } + + protected selectExpandOpenResultNode(node: SearchInWorkspaceResultLineNode): void { + this.model.expandNode(node.parent.parent); + this.model.expandNode(node.parent); + this.model.selectNode(node); + this.model.openNode(node); + } + + protected selectFirstResult(): void { + for (const rootFolder of this.resultTree.values()) { + for (const file of rootFolder.children) { + for (const result of file.children) { + if (SelectableTreeNode.is(result)) { + return this.selectExpandOpenResultNode(result); + } + } + } + } + } + + protected selectLastResult(): void { + const rootFolders = Array.from(this.resultTree.values()); + for (let i = rootFolders.length - 1; i >= 0; i--) { + const rootFolder = rootFolders[i]; + for (let j = rootFolder.children.length - 1; j >= 0; j--) { + const file = rootFolder.children[j]; + for (let k = file.children.length - 1; k >= 0; k--) { + const result = file.children[k]; + if (SelectableTreeNode.is(result)) { + return this.selectExpandOpenResultNode(result); + } + } + } + } + } + /** * Find matches for the given editor. * @param searchTerm the search term. diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx index 9ec19a6cd31fd..78b69f66dfa11 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx @@ -502,7 +502,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge ; } protected handleFocusSearchInputBox = (event: React.FocusEvent) => { - event.target.placeholder = `${SearchInWorkspaceWidget.LABEL} (⇅ ${nls.localizeByDefault('for history')})`; + event.target.placeholder = SearchInWorkspaceWidget.LABEL + nls.localizeByDefault(' ({0} for history)', '⇅'); this.contextKeyService.setSearchInputBoxFocus(true); }; protected handleBlurSearchInputBox = (event: React.FocusEvent) => { @@ -541,7 +541,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge } protected handleFocusReplaceInputBox = (event: React.FocusEvent) => { - event.target.placeholder = `${nls.localizeByDefault('Replace')} (⇅ ${nls.localizeByDefault('for history')})`; + event.target.placeholder = nls.localizeByDefault('Replace') + nls.localizeByDefault(' ({0} for history)', '⇅'); this.contextKeyService.setReplaceInputBoxFocus(true); }; protected handleBlurReplaceInputBox = (event: React.FocusEvent) => { diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts index 70f659b355bd0..d33d0e8c1a736 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts @@ -16,7 +16,7 @@ import { Container } from '@theia/core/shared/inversify'; import { ILogger, isWindows } from '@theia/core'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { RawProcessFactory, RawProcessOptions, RawProcess, ProcessManager } from '@theia/process/lib/node'; import { RipgrepSearchInWorkspaceServer, RgPath } from './ripgrep-search-in-workspace-server'; diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts index a10fc6588a15c..dc55caa616149 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts @@ -18,7 +18,7 @@ import * as fs from '@theia/core/shared/fs-extra'; import * as path from 'path'; import { ILogger } from '@theia/core'; import { RawProcess, RawProcessFactory, RawProcessOptions } from '@theia/process/lib/node'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import URI from '@theia/core/lib/common/uri'; import { inject, injectable } from '@theia/core/shared/inversify'; import { SearchInWorkspaceServer, SearchInWorkspaceOptions, SearchInWorkspaceResult, SearchInWorkspaceClient, LinePreview } from '../common/search-in-workspace-interface'; diff --git a/packages/secondary-window/package.json b/packages/secondary-window/package.json index 821cd7f63fc8f..1513249fc66f7 100644 --- a/packages/secondary-window/package.json +++ b/packages/secondary-window/package.json @@ -1,9 +1,10 @@ { "name": "@theia/secondary-window", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Secondary Window Extension", "dependencies": { - "@theia/core": "1.44.0" + "@theia/core": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -38,6 +39,6 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" } } diff --git a/packages/task/package.json b/packages/task/package.json index eae0a3f2c4558..5d2cc954395e8 100644 --- a/packages/task/package.json +++ b/packages/task/package.json @@ -1,22 +1,23 @@ { "name": "@theia/task", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Task extension. This extension adds support for executing raw or terminal processes in the backend.", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/markers": "1.44.0", - "@theia/monaco": "1.44.0", - "@theia/monaco-editor-core": "1.72.3", - "@theia/process": "1.44.0", - "@theia/terminal": "1.44.0", - "@theia/userstorage": "1.44.0", - "@theia/variable-resolver": "1.44.0", - "@theia/workspace": "1.44.0", + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/markers": "1.54.0", + "@theia/monaco": "1.54.0", + "@theia/monaco-editor-core": "1.83.101", + "@theia/process": "1.54.0", + "@theia/terminal": "1.54.0", + "@theia/userstorage": "1.54.0", + "@theia/variable-resolver": "1.54.0", + "@theia/workspace": "1.54.0", "async-mutex": "^0.3.1", "jsonc-parser": "^2.2.0", - "p-debounce": "^2.1.0" + "p-debounce": "^2.1.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -52,7 +53,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index a16e00dead071..1ff0f329e672a 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -182,6 +182,7 @@ export class QuickOpenTask implements QuickAccessProvider { picker.matchOnDescription = true; picker.ignoreFocusOut = false; picker.items = this.items; + picker.onDidTriggerItemButton(({ item }) => this.onDidTriggerGearIcon(item)); const firstLevelTask = await this.doPickerFirstLevel(picker); @@ -225,7 +226,10 @@ export class QuickOpenTask implements QuickAccessProvider { execute: () => this.showMultiLevelQuickPick(true) })); - this.quickInputService?.showQuickPick(providedTasksItems, { placeholder: CHOOSE_TASK }); + this.quickInputService?.showQuickPick(providedTasksItems, { + placeholder: CHOOSE_TASK, + onDidTriggerItemButton: ({ item }) => this.onDidTriggerGearIcon(item) + }); } attach(): void { diff --git a/packages/task/src/node/task-server.slow-spec.ts b/packages/task/src/node/task-server.slow-spec.ts index 03749c9d2f2aa..4c49bd6e8d272 100644 --- a/packages/task/src/node/task-server.slow-spec.ts +++ b/packages/task/src/node/task-server.slow-spec.ts @@ -72,7 +72,7 @@ describe('Task server / back-end', function (): void { taskServer = testContainer.get(TaskServer); taskServer.setClient(taskWatcher.getTaskClient()); backend = testContainer.get(BackendApplication); - server = await backend.start(); + server = await backend.start(3000, 'localhost'); }); afterEach(async () => { @@ -104,11 +104,11 @@ describe('Task server / back-end', function (): void { await new Promise((resolve, reject) => { const setup = new TestWebSocketChannelSetup({ server, path: `${terminalsPath}/${terminalId}` }); const stringBuffer = new StringBufferingStream(); - setup.multiplexer.onDidOpenChannel(event => { - event.channel.onMessage(e => stringBuffer.push(e().readString())); - event.channel.onError(reject); - event.channel.onClose(() => reject(new Error('Channel has been closed'))); - }); + setup.connectionProvider.listen(`${terminalsPath}/${terminalId}`, (path, channel) => { + channel.onMessage(e => stringBuffer.push(e().readString())); + channel.onError(reject); + channel.onClose(() => reject(new Error('Channel has been closed'))); + }, false); stringBuffer.onData(currentMessage => { // Instead of waiting for one message from the terminal, we wait for several ones as the very first message can be something unexpected. // For instance: `nvm is not compatible with the \"PREFIX\" environment variable: currently set to \"/usr/local\"\r\n` diff --git a/packages/terminal/package.json b/packages/terminal/package.json index 418df815a8bd3..7dd3431211d6e 100644 --- a/packages/terminal/package.json +++ b/packages/terminal/package.json @@ -1,17 +1,19 @@ { "name": "@theia/terminal", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Terminal Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/process": "1.44.0", - "@theia/variable-resolver": "1.44.0", - "@theia/workspace": "1.44.0", - "xterm": "^4.16.0", - "xterm-addon-fit": "^0.5.0", - "xterm-addon-search": "^0.8.2" + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/file-search": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/process": "1.54.0", + "@theia/variable-resolver": "1.54.0", + "@theia/workspace": "1.54.0", + "tslib": "^2.6.2", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", + "xterm-addon-search": "^0.13.0" }, "publishConfig": { "access": "public" @@ -48,7 +50,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/terminal/src/browser/base/terminal-widget.ts b/packages/terminal/src/browser/base/terminal-widget.ts index 9350d0fb32dc7..7c9443f94de7f 100644 --- a/packages/terminal/src/browser/base/terminal-widget.ts +++ b/packages/terminal/src/browser/base/terminal-widget.ts @@ -16,11 +16,12 @@ import { Event, ViewColumn } from '@theia/core'; import { BaseWidget } from '@theia/core/lib/browser'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string'; +import { ThemeIcon } from '@theia/core/lib/common/theme'; import { CommandLineOptions } from '@theia/process/lib/common/shell-command-builder'; import { TerminalSearchWidget } from '../search/terminal-search-widget'; import { TerminalProcessInfo, TerminalExitReason } from '../../common/base-terminal-protocol'; import URI from '@theia/core/lib/common/uri'; -import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string'; export interface TerminalDimensions { cols: number; @@ -48,6 +49,15 @@ export interface TerminalSplitLocation { readonly parentTerminal: string; } +export interface TerminalBuffer { + readonly length: number; + /** + * @param start zero based index of the first line to return + * @param length the max number or lines to return + */ + getLines(start: number, length: number): string[]; +} + /** * Terminal UI widget. */ @@ -118,6 +128,10 @@ export abstract class TerminalWidget extends BaseWidget { /** Event that fires when the terminal input data */ abstract onData: Event; + abstract onOutput: Event; + + abstract buffer: TerminalBuffer; + abstract scrollLineUp(): void; abstract scrollLineDown(): void; @@ -178,9 +192,9 @@ export interface TerminalWidgetOptions { readonly title?: string; /** - * icon class + * icon class with or without color modifier */ - readonly iconClass?: string; + readonly iconClass?: string | ThemeIcon; /** * Path to the executable shell. For example: `/bin/bash`, `bash`, `sh`. diff --git a/packages/terminal/src/browser/terminal-file-link-provider.ts b/packages/terminal/src/browser/terminal-file-link-provider.ts index 5ee6feefe7ffa..d534a9eb9cf47 100644 --- a/packages/terminal/src/browser/terminal-file-link-provider.ts +++ b/packages/terminal/src/browser/terminal-file-link-provider.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { OS, Path } from '@theia/core'; +import { OS, Path, QuickInputService } from '@theia/core'; import { OpenerService } from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { inject, injectable } from '@theia/core/shared/inversify'; @@ -23,12 +23,16 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { TerminalWidget } from './base/terminal-widget'; import { TerminalLink, TerminalLinkProvider } from './terminal-link-provider'; import { TerminalWidgetImpl } from './terminal-widget-impl'; - +import { FileSearchService } from '@theia/file-search/lib/common/file-search-service'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; @injectable() export class FileLinkProvider implements TerminalLinkProvider { @inject(OpenerService) protected readonly openerService: OpenerService; + @inject(QuickInputService) protected readonly quickInputService: QuickInputService; @inject(FileService) protected fileService: FileService; + @inject(FileSearchService) protected searchService: FileSearchService; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; async provideLinks(line: string, terminal: TerminalWidget): Promise { const links: TerminalLink[] = []; @@ -57,10 +61,7 @@ export class FileLinkProvider implements TerminalLinkProvider { const toOpen = await this.toURI(match, await this.getCwd(terminal)); if (toOpen) { // TODO: would be better to ask the opener service, but it returns positively even for unknown files. - try { - const stat = await this.fileService.resolve(toOpen); - return !stat.isDirectory; - } catch { } + return this.isValidFileURI(toOpen); } } catch (err) { console.trace('Error validating ' + match, err); @@ -68,6 +69,14 @@ export class FileLinkProvider implements TerminalLinkProvider { return false; } + protected async isValidFileURI(uri: URI): Promise { + try { + const stat = await this.fileService.resolve(uri); + return !stat.isDirectory; + } catch { } + return false; + } + protected async toURI(match: string, cwd: URI): Promise { const path = await this.extractPath(match); if (!path) { @@ -97,8 +106,11 @@ export class FileLinkProvider implements TerminalLinkProvider { if (!toOpen) { return; } - const position = await this.extractPosition(match); + return this.openURI(toOpen, position); + } + + async openURI(toOpen: URI, position: Position): Promise { let options = {}; if (position) { options = { selection: { start: position } }; @@ -108,7 +120,7 @@ export class FileLinkProvider implements TerminalLinkProvider { const opener = await this.openerService.getOpener(toOpen, options); opener.open(toOpen, options); } catch (err) { - console.error('Cannot open link ' + match, err); + console.error('Cannot open link ' + toOpen, err); } } @@ -120,7 +132,7 @@ export class FileLinkProvider implements TerminalLinkProvider { return info; } - const lineAndColumnMatchIndex = OS.backend.isWindows ? winLineAndColumnMatchIndex : unixLineAndColumnMatchIndex; + const lineAndColumnMatchIndex = this.getLineAndColumnMatchIndex(); for (let i = 0; i < lineAndColumnClause.length; i++) { const lineMatchIndex = lineAndColumnMatchIndex + (lineAndColumnClauseGroupCount * i); const rowNumber = matches[lineMatchIndex]; @@ -137,6 +149,9 @@ export class FileLinkProvider implements TerminalLinkProvider { return info; } + protected getLineAndColumnMatchIndex(): number { + return OS.backend.isWindows ? winLineAndColumnMatchIndex : unixLineAndColumnMatchIndex; + } } @injectable() @@ -153,6 +168,80 @@ export class FileDiffPostLinkProvider extends FileLinkProvider { } } +@injectable() +export class LocalFileLinkProvider extends FileLinkProvider { + override async createRegExp(): Promise { + // match links that might not start with a separator, e.g. 'foo.bar', but don't match single words e.g. 'foo' + const baseLocalUnixLinkClause = + '((' + pathPrefix + '|' + + '(' + excludedPathCharactersClause + '+(' + pathSeparatorClause + '|' + '\\.' + ')' + excludedPathCharactersClause + '+))' + + '(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)*)'; + + const baseLocalWindowsLinkClause = + '((' + winPathPrefix + '|' + + '(' + winExcludedPathCharactersClause + '+(' + winPathSeparatorClause + '|' + '\\.' + ')' + winExcludedPathCharactersClause + '+))' + + '(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)*)'; + + const baseLocalLinkClause = OS.backend.isWindows ? baseLocalWindowsLinkClause : baseLocalUnixLinkClause; + return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`, 'g'); + } + + override async provideLinks(line: string, terminal: TerminalWidget): Promise { + const links: TerminalLink[] = []; + const regExp = await this.createRegExp(); + let regExpResult: RegExpExecArray | null; + while (regExpResult = regExp.exec(line)) { + const match = regExpResult[0]; + const searchTerm = await this.extractPath(match); + if (searchTerm) { + links.push({ + startIndex: regExp.lastIndex - match.length, + length: match.length, + handle: async () => { + const fileUri = await this.isValidWorkspaceFile(searchTerm, terminal); + if (fileUri) { + const position = await this.extractPosition(match); + this.openURI(fileUri, position); + } else { + this.quickInputService.open(match); + } + } + }); + } + } + return links; + } + + protected override getLineAndColumnMatchIndex(): number { + return OS.backend.isWindows ? 14 : 12; + } + + protected async isValidWorkspaceFile(searchTerm: string | undefined, terminal: TerminalWidget): Promise { + if (!searchTerm) { + return undefined; + } + const cwd = await this.getCwd(terminal); + // remove any leading ./, ../ etc. as they can't be searched + searchTerm = searchTerm.replace(/^(\.+[\\/])+/, ''); + const workspaceRoots = this.workspaceService.tryGetRoots().map(root => root.resource.toString()); + // try and find a matching file in the workspace + const files = (await this.searchService.find(searchTerm, { + rootUris: [cwd.toString(), ...workspaceRoots], + fuzzyMatch: true, + limit: 1 + })); + // checks if the string ends in a separator + searchTerm + const regex = new RegExp(`[\\\\|\\/]${searchTerm}$`); + if (files.length && regex.test(files[0])) { + const fileUri = new URI(files[0]); + const valid = await this.isValidFileURI(fileUri); + if (valid) { + return fileUri; + } + } + } +} + // The following regular expressions are taken from: // https://github.com/microsoft/vscode/blob/b118105bf28d773fbbce683f7230d058be2f89a7/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts#L34-L58 diff --git a/packages/terminal/src/browser/terminal-frontend-contribution.ts b/packages/terminal/src/browser/terminal-frontend-contribution.ts index 2263f1402e692..ccd9f9a7254da 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -27,12 +27,13 @@ import { Emitter, Event, ViewColumn, - OS + OS, + CompoundMenuNodeRole } from '@theia/core/lib/common'; import { ApplicationShell, KeybindingContribution, KeyCode, Key, WidgetManager, PreferenceService, KeybindingRegistry, LabelProvider, WidgetOpenerOptions, StorageService, QuickInputService, - codicon, CommonCommands, FrontendApplicationContribution, OnWillStopAction, Dialog, ConfirmDialog, FrontendApplication, PreferenceScope, Widget + codicon, CommonCommands, FrontendApplicationContribution, OnWillStopAction, Dialog, ConfirmDialog, FrontendApplication, PreferenceScope, Widget, SHELL_TABBAR_CONTEXT_MENU } from '@theia/core/lib/browser'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { TERMINAL_WIDGET_FACTORY_ID, TerminalWidgetFactoryOptions, TerminalWidgetImpl } from './terminal-widget-impl'; @@ -55,6 +56,7 @@ import { nls } from '@theia/core/lib/common/nls'; import { Profiles, TerminalPreferences } from './terminal-preferences'; import { ShellTerminalProfile } from './shell-terminal-profile'; import { VariableResolverService } from '@theia/variable-resolver/lib/browser'; +import { Color } from '@theia/core/lib/common/color'; export namespace TerminalMenus { export const TERMINAL = [...MAIN_MENU_BAR, '7_terminal']; @@ -67,6 +69,9 @@ export namespace TerminalMenus { export const TERMINAL_OPEN_EDITORS_CONTEXT_MENU = ['open-editors-context-menu', 'navigation']; export const TERMINAL_CONTEXT_MENU = ['terminal-context-menu']; + export const TERMINAL_CONTRIBUTIONS = [...TERMINAL_CONTEXT_MENU, '5_terminal_contributions']; + + export const TERMINAL_TITLE_CONTRIBUTIONS = [...SHELL_TABBAR_CONTEXT_MENU, 'terminal_title_contributions']; } export namespace TerminalCommands { @@ -307,7 +312,8 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu } else { this.contributedProfileStore.registerTerminalProfile('SHELL', new ShellTerminalProfile(this, { shellPath: await this.resolveShellPath('${SHELL}')!, - shellArgs: ['-l'] + shellArgs: ['-l'], + iconClass: 'codicon codicon-terminal' })); } @@ -637,7 +643,6 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu } protected toggleTerminal(): void { - const terminals = this.shell.getWidgets('bottom').filter(w => w instanceof TerminalWidget); if (terminals.length === 0) { @@ -645,20 +650,17 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu return; } - if (this.shell.bottomPanel.isHidden) { - this.shell.bottomPanel.setHidden(false); + if (!this.shell.isExpanded('bottom')) { + this.shell.expandPanel('bottom'); terminals[0].activate(); - return; - } - - if (this.shell.bottomPanel.isVisible) { + } else { const visibleTerminal = terminals.find(t => t.isVisible); if (!visibleTerminal) { this.shell.bottomPanel.activateWidget(terminals[0]); } else if (this.shell.activeWidget !== visibleTerminal) { this.shell.bottomPanel.activateWidget(visibleTerminal); } else { - this.shell.bottomPanel.setHidden(true); + this.shell.collapsePanel('bottom'); } } @@ -735,6 +737,15 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu menus.registerMenuAction([...TerminalMenus.TERMINAL_CONTEXT_MENU, '_4'], { commandId: TerminalCommands.KILL_TERMINAL.id }); + + menus.registerSubmenu(TerminalMenus.TERMINAL_CONTRIBUTIONS, '', { + role: CompoundMenuNodeRole.Group + }); + + menus.registerSubmenu(TerminalMenus.TERMINAL_TITLE_CONTRIBUTIONS, '', { + role: CompoundMenuNodeRole.Group, + when: 'isTerminalTab' + }); } registerToolbarItems(toolbar: TabBarToolbarRegistry): void { @@ -994,7 +1005,7 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu if (!terminalProfile) { profile = this.profileService.defaultProfile; if (!profile) { - throw new Error('There are not profiles registered'); + throw new Error('There are no profiles registered'); } } @@ -1078,6 +1089,27 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu }, description: 'The selection background color of the terminal.' }); + colors.register({ + id: 'terminal.inactiveSelectionBackground', + defaults: { + light: Color.transparent('terminal.selectionBackground', 0.5), + dark: Color.transparent('terminal.selectionBackground', 0.5), + hcDark: Color.transparent('terminal.selectionBackground', 0.7), + hcLight: Color.transparent('terminal.selectionBackground', 0.5), + }, + description: 'The selection background color of the terminal when it does not have focus.' + }); + colors.register({ + id: 'terminal.selectionForeground', + defaults: { + light: undefined, + dark: undefined, + hcDark: '#000000', + hcLight: '#ffffff' + }, + // eslint-disable-next-line max-len + description: 'The selection foreground color of the terminal. When this is null the selection foreground will be retained and have the minimum contrast ratio feature applied.' + }); colors.register({ id: 'terminal.border', defaults: { diff --git a/packages/terminal/src/browser/terminal-frontend-module.ts b/packages/terminal/src/browser/terminal-frontend-module.ts index e5547eb1d64ad..8f37ce4eef80c 100644 --- a/packages/terminal/src/browser/terminal-frontend-module.ts +++ b/packages/terminal/src/browser/terminal-frontend-module.ts @@ -41,7 +41,7 @@ import { TerminalThemeService } from './terminal-theme-service'; import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access'; import { createXtermLinkFactory, TerminalLinkProvider, TerminalLinkProviderContribution, XtermLinkFactory } from './terminal-link-provider'; import { UrlLinkProvider } from './terminal-url-link-provider'; -import { FileDiffPostLinkProvider, FileDiffPreLinkProvider, FileLinkProvider } from './terminal-file-link-provider'; +import { FileDiffPostLinkProvider, FileDiffPreLinkProvider, FileLinkProvider, LocalFileLinkProvider } from './terminal-file-link-provider'; import { ContributedTerminalProfileStore, DefaultProfileStore, DefaultTerminalProfileService, TerminalProfileService, TerminalProfileStore, UserTerminalProfileStore @@ -123,6 +123,8 @@ export default new ContainerModule(bind => { bind(TerminalLinkProvider).toService(FileDiffPreLinkProvider); bind(FileDiffPostLinkProvider).toSelf().inSingletonScope(); bind(TerminalLinkProvider).toService(FileDiffPostLinkProvider); + bind(LocalFileLinkProvider).toSelf().inSingletonScope(); + bind(TerminalLinkProvider).toService(LocalFileLinkProvider); bind(ContributedTerminalProfileStore).to(DefaultProfileStore).inSingletonScope(); bind(UserTerminalProfileStore).to(DefaultProfileStore).inSingletonScope(); @@ -132,5 +134,5 @@ export default new ContainerModule(bind => { return new DefaultTerminalProfileService(userStore, contributedStore); }).inSingletonScope(); - bind(FrontendApplicationContribution).to(TerminalFrontendContribution); + bind(FrontendApplicationContribution).toService(TerminalFrontendContribution); }); diff --git a/packages/terminal/src/browser/terminal-link-provider.ts b/packages/terminal/src/browser/terminal-link-provider.ts index d6db9be69dfea..3d12f7522cc6f 100644 --- a/packages/terminal/src/browser/terminal-link-provider.ts +++ b/packages/terminal/src/browser/terminal-link-provider.ts @@ -25,7 +25,7 @@ import { TerminalWidgetImpl } from './terminal-widget-impl'; export const TerminalLinkProvider = Symbol('TerminalLinkProvider'); export interface TerminalLinkProvider { - provideLinks(line: string, terminal: TerminalWidget, cancelationToken?: CancellationToken): Promise; + provideLinks(line: string, terminal: TerminalWidget, cancellationToken?: CancellationToken): Promise; } export const TerminalLink = Symbol('TerminalLink'); diff --git a/packages/terminal/src/browser/terminal-preferences.ts b/packages/terminal/src/browser/terminal-preferences.ts index ec1fe0592af0e..fbc5dd49632dc 100644 --- a/packages/terminal/src/browser/terminal-preferences.ts +++ b/packages/terminal/src/browser/terminal-preferences.ts @@ -34,7 +34,7 @@ const commonProfileProperties: PreferenceSchemaProperties = { }, overrideName: { type: 'boolean', - description: nls.localizeByDefault('Controls whether or not the profile name overrides the auto detected one.') + description: nls.localizeByDefault('Whether or not to replace the dynamic terminal title that detects what program is running with the static profile name.') }, icon: { type: 'string', @@ -134,7 +134,8 @@ export const TerminalConfigSchema: PreferenceSchema = { description: nls.localize('theia/terminal/rendererType', 'Controls how the terminal is rendered.'), type: 'string', enum: ['canvas', 'dom'], - default: 'canvas' + default: 'canvas', + deprecationMessage: nls.localize('theia/terminal/rendererTypeDeprecationMessage', 'The renderer type is no longer supported as an option.') }, 'terminal.integrated.copyOnSelection': { description: nls.localizeByDefault('Controls whether text selected in the terminal will be copied to the clipboard.'), diff --git a/packages/terminal/src/browser/terminal-theme-service.ts b/packages/terminal/src/browser/terminal-theme-service.ts index 3603f8f30f643..dc74e80765106 100644 --- a/packages/terminal/src/browser/terminal-theme-service.ts +++ b/packages/terminal/src/browser/terminal-theme-service.ts @@ -187,14 +187,18 @@ export class TerminalThemeService { const backgroundColor = this.colorRegistry.getCurrentColor('terminal.background') || this.colorRegistry.getCurrentColor('panel.background'); const cursorColor = this.colorRegistry.getCurrentColor('terminalCursor.foreground') || foregroundColor; const cursorAccentColor = this.colorRegistry.getCurrentColor('terminalCursor.background') || backgroundColor; - const selectionColor = this.colorRegistry.getCurrentColor('terminal.selectionBackground'); + const selectionBackgroundColor = this.colorRegistry.getCurrentColor('terminal.selectionBackground'); + const selectionInactiveBackground = this.colorRegistry.getCurrentColor('terminal.inactiveSelectionBackground'); + const selectionForegroundColor = this.colorRegistry.getCurrentColor('terminal.selectionForeground'); const theme: ITheme = { background: backgroundColor, foreground: foregroundColor, cursor: cursorColor, cursorAccent: cursorAccentColor, - selection: selectionColor + selectionBackground: selectionBackgroundColor, + selectionInactiveBackground: selectionInactiveBackground, + selectionForeground: selectionForegroundColor }; // eslint-disable-next-line guard-for-in for (const id in terminalAnsiColorMap) { diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index 5cb3dd8755d22..5b923a278ce1c 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -14,12 +14,13 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Terminal, RendererType } from 'xterm'; +import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; -import { ContributionProvider, Disposable, Event, Emitter, ILogger, DisposableCollection, Channel, OS } from '@theia/core'; +import { ContributionProvider, Disposable, Event, Emitter, ILogger, DisposableCollection, Channel, OS, generateUuid } from '@theia/core'; import { - Widget, Message, WebSocketConnectionProvider, StatefulWidget, isFirefox, MessageLoop, KeyCode, codicon, ExtractableWidget, ContextMenuRenderer + Widget, Message, StatefulWidget, isFirefox, MessageLoop, KeyCode, ExtractableWidget, ContextMenuRenderer, + DecorationStyle } from '@theia/core/lib/browser'; import { isOSX } from '@theia/core/lib/common'; import { WorkspaceService } from '@theia/workspace/lib/browser'; @@ -29,10 +30,11 @@ import { IBaseTerminalServer, TerminalProcessInfo, TerminalExitReason } from '.. import { TerminalWatcher } from '../common/terminal-watcher'; import { TerminalWidgetOptions, TerminalWidget, TerminalDimensions, TerminalExitStatus, TerminalLocationOptions, - TerminalLocation + TerminalLocation, + TerminalBuffer } from './base/terminal-widget'; import { Deferred } from '@theia/core/lib/common/promise-util'; -import { TerminalPreferences, TerminalRendererType, isTerminalRendererType, DEFAULT_TERMINAL_RENDERER_TYPE, CursorStyle } from './terminal-preferences'; +import { TerminalPreferences } from './terminal-preferences'; import URI from '@theia/core/lib/common/uri'; import { TerminalService } from './base/terminal-service'; import { TerminalSearchWidgetFactory, TerminalSearchWidget } from './search/terminal-search-widget'; @@ -46,6 +48,8 @@ import debounce = require('p-debounce'); import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string'; import { EnhancedPreviewWidget } from '@theia/core/lib/browser/widgets/enhanced-preview-widget'; import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; +import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider'; +import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; export const TERMINAL_WIDGET_FACTORY_ID = 'terminal'; @@ -59,6 +63,23 @@ export interface TerminalContribution { onCreate(term: TerminalWidgetImpl): void; } +class TerminalBufferImpl implements TerminalBuffer { + constructor(private readonly term: Terminal) { + } + + get length(): number { + return this.term.buffer.active.length; + }; + getLines(start: number, length: number): string[] { + const result: string[] = []; + for (let i = 0; i < length && this.length - 1 - i >= 0; i++) { + result.push(this.term.buffer.active.getLine(this.length - 1 - i)!.translateToString()); + } + return result; + } + +} + @injectable() export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget, ExtractableWidget, EnhancedPreviewWidget { readonly isExtractable: boolean = true; @@ -85,10 +106,11 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget protected isAttachedCloseListener: boolean = false; protected shown = false; protected enhancedPreviewNode: Node | undefined; + protected styleElement: HTMLStyleElement | undefined; override lastCwd = new URI(); @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; - @inject(WebSocketConnectionProvider) protected readonly webSocketConnectionProvider: WebSocketConnectionProvider; + @inject(RemoteConnectionProvider) protected readonly connectionProvider: ServiceConnectionProvider; @inject(TerminalWidgetOptions) options: TerminalWidgetOptions; @inject(ShellTerminalServerProxy) protected readonly shellTerminalServer: ShellTerminalServerProxy; @inject(TerminalWatcher) protected readonly terminalWatcher: TerminalWatcher; @@ -100,6 +122,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget @inject(TerminalSearchWidgetFactory) protected readonly terminalSearchBoxFactory: TerminalSearchWidgetFactory; @inject(TerminalCopyOnSelectionHandler) protected readonly copyOnSelectionHandler: TerminalCopyOnSelectionHandler; @inject(TerminalThemeService) protected readonly themeService: TerminalThemeService; + @inject(ColorRegistry) protected readonly colorRegistry: ColorRegistry; @inject(ShellCommandBuilder) protected readonly shellCommandBuilder: ShellCommandBuilder; @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; @inject(MarkdownRendererFactory) protected readonly markdownRendererFactory: MarkdownRendererFactory; @@ -122,6 +145,9 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget protected readonly onDataEmitter = new Emitter(); readonly onData: Event = this.onDataEmitter.event; + protected readonly onOutputEmitter = new Emitter(); + readonly onOutput: Event = this.onOutputEmitter.event; + protected readonly onKeyEmitter = new Emitter<{ key: string, domEvent: KeyboardEvent }>(); readonly onKey: Event<{ key: string, domEvent: KeyboardEvent }> = this.onKeyEmitter.event; @@ -133,15 +159,15 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget protected readonly toDisposeOnConnect = new DisposableCollection(); + private _buffer: TerminalBuffer; + override get buffer(): TerminalBuffer { + return this._buffer; + } + @postConstruct() protected init(): void { this.setTitle(this.options.title || TerminalWidgetImpl.LABEL); - - if (this.options.iconClass) { - this.title.iconClass = this.options.iconClass; - } else { - this.title.iconClass = codicon('terminal'); - } + this.setIconClass(); if (this.options.kind) { this.terminalKind = this.options.kind; @@ -160,7 +186,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.term = new Terminal({ cursorBlink: this.preferences['terminal.integrated.cursorBlinking'], - cursorStyle: this.getCursorStyle(), + cursorStyle: this.preferences['terminal.integrated.cursorStyle'] === 'line' ? 'bar' : this.preferences['terminal.integrated.cursorStyle'], cursorWidth: this.preferences['terminal.integrated.cursorWidth'], fontFamily: this.preferences['terminal.integrated.fontFamily'], fontSize: this.preferences['terminal.integrated.fontSize'], @@ -171,9 +197,9 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget lineHeight: this.preferences['terminal.integrated.lineHeight'], scrollback: this.preferences['terminal.integrated.scrollback'], fastScrollSensitivity: this.preferences['terminal.integrated.fastScrollSensitivity'], - rendererType: this.getTerminalRendererType(this.preferences['terminal.integrated.rendererType']), theme: this.themeService.theme }); + this._buffer = new TerminalBufferImpl(this.term); this.fitAddon = new FitAddon(); this.term.loadAddon(this.fitAddon); @@ -181,34 +207,15 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.initializeLinkHover(); this.toDispose.push(this.preferences.onPreferenceChanged(change => { - const lastSeparator = change.preferenceName.lastIndexOf('.'); - if (lastSeparator > 0) { - let preferenceName = change.preferenceName.substring(lastSeparator + 1); - let preferenceValue = change.newValue; - - if (preferenceName === 'rendererType') { - const newRendererType = preferenceValue as string; - if (newRendererType !== this.getTerminalRendererType(newRendererType)) { - // Given terminal renderer type is not supported or invalid - preferenceValue = DEFAULT_TERMINAL_RENDERER_TYPE; - } - } else if (preferenceName === 'cursorBlinking') { - // Convert the terminal preference into a valid `xterm` option - preferenceName = 'cursorBlink'; - } else if (preferenceName === 'cursorStyle') { - preferenceValue = this.getCursorStyle(); - } - try { - this.term.setOption(preferenceName, preferenceValue); - } catch (e) { - console.debug(`xterm configuration: '${preferenceName}' with value '${preferenceValue}' is not valid.`); - } - this.needsResize = true; - this.update(); - } + this.updateConfig(); + this.needsResize = true; + this.update(); })); - this.toDispose.push(this.themeService.onDidChange(() => this.term.setOption('theme', this.themeService.theme))); + this.toDispose.push(this.themeService.onDidChange(() => { + this.term.options.theme = this.themeService.theme; + this.setIconClass(); + })); this.attachCustomKeyEventHandler(); const titleChangeListenerDispose = this.term.onTitleChange((title: string) => { if (this.options.useServerTitle) { @@ -313,25 +320,63 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget return this.terminalKind; } - /** - * Get the cursor style compatible with `xterm`. - * @returns CursorStyle - */ - private getCursorStyle(): CursorStyle { - const value = this.preferences['terminal.integrated.cursorStyle']; - return value === 'line' ? 'bar' : value; + updateConfig(): void { + this.setCursorBlink(this.preferences.get('terminal.integrated.cursorBlinking')); + this.setCursorStyle(this.preferences.get('terminal.integrated.cursorStyle')); + this.setCursorWidth(this.preferences.get('terminal.integrated.cursorWidth')); + this.term.options.fontFamily = this.preferences.get('terminal.integrated.fontFamily'); + this.term.options.fontSize = this.preferences.get('terminal.integrated.fontSize'); + this.term.options.fontWeight = this.preferences.get('terminal.integrated.fontWeight'); + this.term.options.fontWeightBold = this.preferences.get('terminal.integrated.fontWeightBold'); + this.term.options.drawBoldTextInBrightColors = this.preferences.get('terminal.integrated.drawBoldTextInBrightColors'); + this.term.options.letterSpacing = this.preferences.get('terminal.integrated.letterSpacing'); + this.term.options.lineHeight = this.preferences.get('terminal.integrated.lineHeight'); + this.term.options.scrollback = this.preferences.get('terminal.integrated.scrollback'); + this.term.options.fastScrollSensitivity = this.preferences.get('terminal.integrated.fastScrollSensitivity'); } - /** - * Returns given renderer type if it is valid and supported or default renderer otherwise. - * - * @param terminalRendererType desired terminal renderer type - */ - private getTerminalRendererType(terminalRendererType?: string | TerminalRendererType): RendererType { - if (terminalRendererType && isTerminalRendererType(terminalRendererType)) { - return terminalRendererType; + protected setIconClass(): void { + this.styleElement?.remove(); + if (this.options.iconClass) { + const iconClass = this.options.iconClass; + if (typeof iconClass === 'string') { + this.title.iconClass = iconClass; + } else { + const iconClasses: string[] = []; + iconClasses.push(iconClass.id); + if (iconClass.color) { + this.styleElement = DecorationStyle.createStyleElement(`${this.terminalId}-terminal-style`); + const classId = 'terminal-icon-' + generateUuid().replace(/-/g, ''); + const color = this.colorRegistry.getCurrentColor(iconClass.color.id); + this.styleElement.textContent = ` + .${classId}::before { + color: ${color}; + } + `; + iconClasses.push(classId); + } + this.title.iconClass = iconClasses.join(' '); + } + } + } + + private setCursorBlink(blink: boolean): void { + if (this.term.options.cursorBlink !== blink) { + this.term.options.cursorBlink = blink; + this.term.refresh(0, this.term.rows - 1); + } + } + + private setCursorStyle(style: 'block' | 'underline' | 'bar' | 'line'): void { + if (this.term.options.cursorStyle !== style) { + this.term.options.cursorStyle = (style === 'line') ? 'bar' : style; + } + } + + private setCursorWidth(width: number): void { + if (this.term.options.cursorWidth !== width) { + this.term.options.cursorWidth = width; } - return DEFAULT_TERMINAL_RENDERER_TYPE; } protected initializeLinkHover(): void { @@ -610,8 +655,6 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget if (this.needsResize) { this.resizeTerminal(); this.needsResize = false; - - this.resizeTerminalProcess(); } } @@ -629,9 +672,9 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.toDisposeOnConnect.dispose(); this.toDispose.push(this.toDisposeOnConnect); const waitForConnection = this.waitForConnection = new Deferred(); - this.webSocketConnectionProvider.listen({ - path: `${terminalsPath}/${this.terminalId}`, - onConnection: connection => { + this.connectionProvider.listen( + `${terminalsPath}/${this.terminalId}`, + (path, connection) => { connection.onMessage(e => { this.write(e().readString()); }); @@ -652,8 +695,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget if (waitForConnection) { waitForConnection.resolve(connection); } - } - }, { reconnecting: false }); + }, false); } protected async reconnectTerminalProcess(): Promise { if (this.options.isPseudoTerminal) { @@ -672,16 +714,34 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget } this.term.open(this.node); + interface ViewportType { + register(d: Disposable): void; + _refreshAnimationFrame: number | null; + _coreBrowserService: { + window: Window; + } + } + + // Workaround for https://github.com/xtermjs/xterm.js/issues/4775. Can be removed for releases > 5.3.0 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const viewPort: ViewportType = (this.term as any)._core.viewport; + viewPort.register(Disposable.create(() => { + if (typeof viewPort._refreshAnimationFrame === 'number') { + viewPort._coreBrowserService.window.cancelAnimationFrame(viewPort._refreshAnimationFrame); + } + })); + if (isFirefox) { // monkey patching intersection observer handling for secondary window support // eslint-disable-next-line @typescript-eslint/no-explicit-any const renderService: any = (this.term as any)._core._renderService; - const originalFunc: (entry: IntersectionObserverEntry) => void = renderService._onIntersectionChange.bind(renderService); + + const originalFunc: (entry: IntersectionObserverEntry) => void = renderService._handleIntersectionChange.bind(renderService); const replacement = function (entry: IntersectionObserverEntry): void { if (entry.target.ownerDocument !== document) { // in Firefox, the intersection observer always reports the widget as non-intersecting if the dom element // is in a different document from when the IntersectionObserver started observing. Since we know - // that the widget is always "visible" when in a secondary window, so we mark the entry as "intersecting" + // that the widget is always "visible" when in a secondary window, so we refresh the rows ourselves const patchedEvent: IntersectionObserverEntry = { ...entry, isIntersecting: true, @@ -692,7 +752,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget } }; - renderService._onIntersectionChange = replacement; + renderService._handleIntersectionChange = replacement.bind(renderService); } if (this.initialData) { @@ -700,18 +760,12 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget } this.termOpened = true; this.initialData = ''; - - if (isFirefox) { - // The software scrollbars don't work with xterm.js, so we disable the scrollbar if we are on firefox. - if (this.term.element) { - (this.term.element.children.item(0) as HTMLElement).style.overflow = 'hidden'; - } - } } write(data: string): void { if (this.termOpened) { this.term.write(data); + this.onOutputEmitter.fire(data); } else { this.initialData += data; } @@ -763,6 +817,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget writeLine(text: string): void { this.term.writeln(text); + this.onOutputEmitter.fire(text + '\n'); } get onTerminalDidClose(): Event { @@ -784,6 +839,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget // don't use preview node anymore. rendered markdown will be disposed on super call this.enhancedPreviewNode = undefined; } + this.styleElement?.remove(); super.dispose(); } @@ -794,9 +850,13 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget return; } const geo = this.fitAddon.proposeDimensions(); - const cols = geo.cols; - const rows = geo.rows - 1; // subtract one row for margin - this.term.resize(cols, rows); + if (geo) { + const cols = geo.cols; + const rows = geo.rows - 1; // subtract one row for margin + this.term.resize(cols, rows); + + this.resizeTerminalProcess(); + } } protected resizeTerminalProcess(): void { diff --git a/packages/terminal/src/node/shell-process.ts b/packages/terminal/src/node/shell-process.ts index 524a697082c7e..1efab68ed7c3e 100644 --- a/packages/terminal/src/node/shell-process.ts +++ b/packages/terminal/src/node/shell-process.ts @@ -20,7 +20,7 @@ import { ILogger } from '@theia/core/lib/common/logger'; import { TerminalProcess, TerminalProcessOptions, ProcessManager, MultiRingBuffer } from '@theia/process/lib/node'; import { isWindows, isOSX } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { EnvironmentUtils } from '@theia/core/lib/node/environment-utils'; import { parseArgs } from '@theia/process/lib/node/utils'; @@ -61,15 +61,16 @@ export class ShellProcess extends TerminalProcess { @inject(ILogger) @named('terminal') logger: ILogger, @inject(EnvironmentUtils) environmentUtils: EnvironmentUtils, ) { + const env = { 'COLORTERM': 'truecolor' }; super({ command: options.shell || ShellProcess.getShellExecutablePath(), args: options.args || ShellProcess.getShellExecutableArgs(), options: { - name: 'xterm-color', + name: 'xterm-256color', cols: options.cols || ShellProcess.defaultCols, rows: options.rows || ShellProcess.defaultRows, cwd: getRootPath(options.rootURI), - env: options.strictEnv !== true ? environmentUtils.mergeProcessEnv(options.env) : options.env, + env: options.strictEnv !== true ? Object.assign(env, environmentUtils.mergeProcessEnv(options.env)) : Object.assign(env, options.env), }, isPseudo: options.isPseudo, }, processManager, ringBuffer, logger); diff --git a/packages/terminal/src/node/shell-terminal-server.ts b/packages/terminal/src/node/shell-terminal-server.ts index 63eded6a30618..bb2270f162574 100644 --- a/packages/terminal/src/node/shell-terminal-server.ts +++ b/packages/terminal/src/node/shell-terminal-server.ts @@ -69,7 +69,9 @@ export class ShellTerminalServer extends BaseTerminalServer implements IShellTer private spawnAsPromised(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { let stdout = ''; - const child = cp.spawn(command, args); + const child = cp.spawn(command, args, { + shell: true + }); if (child.pid) { child.stdout.on('data', (data: Buffer) => { stdout += data.toString(); diff --git a/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts b/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts index cf9defadd94a9..c9b3b6316549f 100644 --- a/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts +++ b/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts @@ -32,7 +32,7 @@ describe('Terminal Backend Contribution', function (): void { const container = createTerminalTestContainer(); const application = container.get(BackendApplication); shellTerminalServer = container.get(IShellTerminalServer); - server = await application.start(); + server = await application.start(3000, 'localhost'); }); afterEach(() => { @@ -46,16 +46,16 @@ describe('Terminal Backend Contribution', function (): void { const terminalId = await shellTerminalServer.create({}); await new Promise((resolve, reject) => { const path = `${terminalsPath}/${terminalId}`; - const { channel, multiplexer } = new TestWebSocketChannelSetup({ server, path }); - channel.onError(reject); - channel.onClose(event => reject(new Error(`channel is closed with '${event.code}' code and '${event.reason}' reason}`))); + const { connectionProvider } = new TestWebSocketChannelSetup({ server, path }); - multiplexer.onDidOpenChannel(event => { - if (event.id === path) { + connectionProvider.listen(path, (path2, channel) => { + channel.onError(reject); + channel.onClose(event => reject(new Error(`channel is closed with '${event.code}' code and '${event.reason}' reason}`))); + if (path2 === path) { resolve(); channel.close(); } - }); + }, false); }); }); diff --git a/packages/terminal/src/node/terminal-backend-contribution.ts b/packages/terminal/src/node/terminal-backend-contribution.ts index d538ff697bd62..07649685cf377 100644 --- a/packages/terminal/src/node/terminal-backend-contribution.ts +++ b/packages/terminal/src/node/terminal-backend-contribution.ts @@ -32,7 +32,7 @@ export class TerminalBackendContribution implements MessagingService.Contributio protected readonly logger: ILogger; configure(service: MessagingService): void { - service.wsChannel(`${terminalsPath}/:id`, (params: { id: string }, channel) => { + service.registerChannelHandler(`${terminalsPath}/:id`, (params: { id: string }, channel) => { const id = parseInt(params.id, 10); const termProcess = this.processManager.get(id); if (termProcess instanceof TerminalProcess) { diff --git a/packages/terminal/tsconfig.json b/packages/terminal/tsconfig.json index adb72fecde9d5..e612a846eb047 100644 --- a/packages/terminal/tsconfig.json +++ b/packages/terminal/tsconfig.json @@ -15,6 +15,9 @@ { "path": "../editor" }, + { + "path": "../file-search" + }, { "path": "../filesystem" }, diff --git a/packages/test/package.json b/packages/test/package.json index dc622b789f01f..74637739d8f82 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,13 +1,13 @@ { "name": "@theia/test", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Test Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/navigator": "1.44.0", - "@theia/terminal": "1.44.0", + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/navigator": "1.54.0", + "@theia/terminal": "1.54.0", "xterm": "^4.16.0", "xterm-addon-fit": "^0.5.0" }, @@ -44,7 +44,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/test/src/browser/style/index.css b/packages/test/src/browser/style/index.css index f919706ee4318..c880f8c433ac4 100644 --- a/packages/test/src/browser/style/index.css +++ b/packages/test/src/browser/style/index.css @@ -18,25 +18,29 @@ } .theia-test-view .passed, -.theia-test-result-view .passed { +.theia-test-run-view .passed { color: var(--theia-successBackground); } .theia-test-view .failed, -.theia-test-result-view .failed { +.theia-test-run-view .failed { color: var(--theia-editorError-foreground); } .theia-test-view .errored, -.theia-test-result-view .errored { +.theia-test-run-view .errored { color: var(--theia-editorError-foreground); } .theia-test-view .queued, -.theia-test-result-view .queued { +.theia-test-run-view .queued { color: var(--theia-editorWarning-foreground); } +.theia-test-result-view .debug-frame { + white-space: pre; +} + .theia-test-view .theia-TreeNode:not(:hover):not(.theia-mod-selected) .theia-test-tree-inline-action { display: none; } \ No newline at end of file diff --git a/packages/test/src/browser/test-execution-progress-service.ts b/packages/test/src/browser/test-execution-progress-service.ts new file mode 100644 index 0000000000000..c0e85aa698981 --- /dev/null +++ b/packages/test/src/browser/test-execution-progress-service.ts @@ -0,0 +1,53 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { Widget } from '@theia/core/lib/browser'; +import { TestResultViewContribution } from './view/test-result-view-contribution'; +import { TestViewContribution } from './view/test-view-contribution'; +import { TestPreferences } from './test-preferences'; + +export interface TestExecutionProgressService { + onTestRunRequested(preserveFocus: boolean): Promise; +} + +export const TestExecutionProgressService = Symbol('TestExecutionProgressService'); + +@injectable() +export class DefaultTestExecutionProgressService implements TestExecutionProgressService { + + @inject(TestResultViewContribution) + protected readonly testResultView: TestResultViewContribution; + + @inject(TestViewContribution) + protected readonly testView: TestViewContribution; + + @inject(TestPreferences) + protected readonly testPreferences: TestPreferences; + + async onTestRunRequested(preserveFocus: boolean): Promise { + if (!preserveFocus) { + const openTesting = this.testPreferences['testing.openTesting']; + if (openTesting === 'openOnTestStart') { + this.openTestResultView(); + } + } + } + + async openTestResultView(): Promise { + return this.testResultView.openView({ activate: true }); + } +} diff --git a/packages/test/src/browser/test-preferences.ts b/packages/test/src/browser/test-preferences.ts new file mode 100644 index 0000000000000..ad5677d6290e2 --- /dev/null +++ b/packages/test/src/browser/test-preferences.ts @@ -0,0 +1,58 @@ +// ***************************************************************************** +// 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 { interfaces } from '@theia/core/shared/inversify'; +import { createPreferenceProxy, PreferenceProxy, PreferenceService, PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser'; +import { nls } from '@theia/core/lib/common/nls'; + +export const TestConfigSchema: PreferenceSchema = { + type: 'object', + properties: { + 'testing.openTesting': { + type: 'string', + enum: ['neverOpen', 'openOnTestStart'], + enumDescriptions: [ + nls.localizeByDefault('Never automatically open the testing views'), + nls.localizeByDefault('Open the test results view when tests start'), + ], + description: nls.localizeByDefault('Controls when the testing view should open.'), + default: 'neverOpen', + scope: 'resource', + } + } +}; + +export interface TestConfiguration { + 'testing.openTesting': 'neverOpen' | 'openOnTestStart'; +} + +export const TestPreferenceContribution = Symbol('TestPreferenceContribution'); +export const TestPreferences = Symbol('TestPreferences'); +export type TestPreferences = PreferenceProxy; + +export function createTestPreferences(preferences: PreferenceService, schema: PreferenceSchema = TestConfigSchema): TestPreferences { + return createPreferenceProxy(preferences, schema); +} + +export const bindTestPreferences = (bind: interfaces.Bind): void => { + bind(TestPreferences).toDynamicValue(ctx => { + const preferences = ctx.container.get(PreferenceService); + const contribution = ctx.container.get(TestPreferenceContribution); + return createTestPreferences(preferences, contribution.schema); + }).inSingletonScope(); + bind(TestPreferenceContribution).toConstantValue({ schema: TestConfigSchema }); + bind(PreferenceContribution).toService(TestPreferenceContribution); +}; diff --git a/packages/test/src/browser/test-service.ts b/packages/test/src/browser/test-service.ts index e7aba373d23b8..c2fb29f2744d0 100644 --- a/packages/test/src/browser/test-service.ts +++ b/packages/test/src/browser/test-service.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { CancellationToken, ContributionProvider, Disposable, Emitter, Event, QuickPickService, isObject, nls } from '@theia/core/lib/common'; -import { CancellationTokenSource, Location, Range } from '@theia/core/shared/vscode-languageserver-protocol'; +import { CancellationTokenSource, Location, Range, Position, DocumentUri } from '@theia/core/shared/vscode-languageserver-protocol'; import { CollectionDelta, TreeDelta } from '../common/tree-delta'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; import URI from '@theia/core/lib/common/uri'; @@ -32,10 +32,10 @@ export enum TestRunProfileKind { export interface TestRunProfile { readonly kind: TestRunProfileKind; readonly label: string, - readonly isDefault: boolean; + isDefault: boolean; readonly canConfigure: boolean; readonly tag: string; - run(name: string, included: readonly TestItem[], excluded: readonly TestItem[]): void; + run(name: string, included: readonly TestItem[], excluded: readonly TestItem[], preserveFocus: boolean): void; configure(): void; } @@ -56,8 +56,22 @@ export enum TestExecutionState { export interface TestMessage { readonly expected?: string; readonly actual?: string; - readonly location: Location; + readonly location?: Location; readonly message: string | MarkdownString; + readonly contextValue?: string; + readonly stackTrace?: TestMessageStackFrame[]; +} + +export interface TestMessageStackFrame { + readonly label: string, + readonly uri?: DocumentUri, + readonly position?: Position, +} + +export namespace TestMessage { + export function is(obj: unknown): obj is TestMessage { + return isObject(obj) && (MarkdownString.is(obj.message) || typeof obj.message === 'string'); + } } export interface TestState { @@ -89,6 +103,7 @@ export interface TestStateChangedEvent { export interface TestRun { cancel(): void; + readonly id: string; readonly name: string; readonly isRunning: boolean; readonly controller: TestController; @@ -136,6 +151,7 @@ export interface TestItem { readonly controller: TestController | undefined; readonly canResolveChildren: boolean; resolveChildren(): void; + readonly path: string[]; } export namespace TestItem { @@ -169,6 +185,7 @@ export interface TestController { export interface TestService { clearResults(): void; configureProfile(): void; + selectDefaultProfile(): void; runTestsWithProfile(tests: TestItem[]): void; runTests(profileKind: TestRunProfileKind, tests: TestItem[]): void; runAllTests(profileKind: TestRunProfileKind): void; @@ -182,6 +199,20 @@ export interface TestService { onDidChangeIsRefreshing: Event; } +export namespace TestServices { + export function withTestRun(service: TestService, controllerId: string, runId: string): TestRun { + const controller = service.getControllers().find(c => c.id === controllerId); + if (!controller) { + throw new Error(`No test controller with id '${controllerId}' found`); + } + const run = controller.testRuns.find(r => r.id === runId); + if (!run) { + throw new Error(`No test run with id '${runId}' found`); + } + return run; + } +} + export const TestContribution = Symbol('TestContribution'); export interface TestContribution { @@ -278,7 +309,7 @@ export class DefaultTestService implements TestService { } } if (activeProfile) { - activeProfile.run(`Test run #${this.testRunCounter++}`, items, []); + activeProfile.run(`Test run #${this.testRunCounter++}`, items, [], true); } } @@ -296,7 +327,7 @@ export class DefaultTestService implements TestService { } return { iconClasses, - label: profile.label, + label: `${profile.label}${profile.isDefault ? ' (default)' : ''}`, profile: profile }; }); @@ -305,6 +336,22 @@ export class DefaultTestService implements TestService { } + protected async pickProfileKind(): Promise { + // eslint-disable-next-line arrow-body-style + const picks = [{ + iconClasses: codiconArray('run'), + label: 'Run', + kind: TestRunProfileKind.Run + }, { + iconClasses: codiconArray('debug-alt'), + label: 'Debug', + kind: TestRunProfileKind.Debug + }]; + + return (await this.quickpickService.show(picks, { title: 'Select the kind of profiles' }))?.kind; + + } + runTests(profileKind: TestRunProfileKind, items: TestItem[]): void { groupBy(items, item => item.controller).forEach((tests, controller) => { if (controller) { @@ -318,13 +365,28 @@ export class DefaultTestService implements TestService { if (controller) { this.pickProfile(controller.testRunProfiles, nls.localizeByDefault('Pick a test profile to use')).then(activeProfile => { if (activeProfile) { - activeProfile.run(`Test run #${this.testRunCounter++}`, items, []); + activeProfile.run(`Test run #${this.testRunCounter++}`, items, [], true); } }); } }); } + selectDefaultProfile(): void { + this.pickProfileKind().then(kind => { + const profiles = this.getControllers().flatMap(c => c.testRunProfiles).filter(profile => profile.kind === kind); + this.pickProfile(profiles, nls.localizeByDefault('Pick a test profile to use')).then(activeProfile => { + if (activeProfile) { + // only change the default for the controller containing selected profile for default and its profiles with same kind + const controller = this.getControllers().find(c => c.testRunProfiles.includes(activeProfile)); + controller?.testRunProfiles.filter(profile => profile.kind === activeProfile.kind).forEach(profile => { + profile.isDefault = profile === activeProfile; + }); + } + }); + }); + } + configureProfile(): void { const profiles: TestRunProfile[] = []; diff --git a/packages/test/src/browser/view/test-context-key-service.ts b/packages/test/src/browser/view/test-context-key-service.ts new file mode 100644 index 0000000000000..866b048f747ac --- /dev/null +++ b/packages/test/src/browser/view/test-context-key-service.ts @@ -0,0 +1,36 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; +import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service'; + +@injectable() +export class TestContextKeyService { + + @inject(ContextKeyService) + protected readonly contextKeyService: ContextKeyService; + + protected _contextValue: ContextKey; + get contextValue(): ContextKey { + return this._contextValue; + } + + @postConstruct() + protected init(): void { + this._contextValue = this.contextKeyService.createKey('testMessage', undefined); + } + +} diff --git a/packages/test/src/browser/view/test-output-ui-model.ts b/packages/test/src/browser/view/test-output-ui-model.ts index 87d6f586bb053..f9520d71b337e 100644 --- a/packages/test/src/browser/view/test-output-ui-model.ts +++ b/packages/test/src/browser/view/test-output-ui-model.ts @@ -15,8 +15,9 @@ // ***************************************************************************** import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; -import { TestController, TestOutputItem, TestRun, TestService, TestState, TestStateChangedEvent } from '../test-service'; +import { TestController, TestFailure, TestOutputItem, TestRun, TestService, TestState, TestStateChangedEvent } from '../test-service'; import { Disposable, Emitter, Event } from '@theia/core'; +import { TestContextKeyService } from './test-context-key-service'; export interface ActiveRunEvent { controller: TestController; @@ -41,6 +42,7 @@ interface ActiveTestRunInfo { @injectable() export class TestOutputUIModel { + @inject(TestContextKeyService) protected readonly testContextKeys: TestContextKeyService; @inject(TestService) protected testService: TestService; protected readonly activeRuns = new Map(); @@ -139,6 +141,12 @@ export class TestOutputUIModel { set selectedTestState(element: TestState | undefined) { if (element !== this._selectedTestState) { this._selectedTestState = element; + if (this._selectedTestState && TestFailure.is(this._selectedTestState.state)) { + const message = this._selectedTestState.state.messages[0]; + this.testContextKeys.contextValue.set(message.contextValue); + } else { + this.testContextKeys.contextValue.reset(); + } this.onDidChangeSelectedTestStateEmitter.fire(element); } } diff --git a/packages/test/src/browser/view/test-result-widget.ts b/packages/test/src/browser/view/test-result-widget.ts index 4df73d7914e97..a1f154acc9f50 100644 --- a/packages/test/src/browser/view/test-result-widget.ts +++ b/packages/test/src/browser/view/test-result-widget.ts @@ -14,14 +14,17 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { BaseWidget, Message, codicon } from '@theia/core/lib/browser'; +import { BaseWidget, LabelProvider, Message, OpenerService, codicon } from '@theia/core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { TestOutputUIModel } from './test-output-ui-model'; import { DisposableCollection, nls } from '@theia/core'; -import { TestFailure, TestMessage } from '../test-service'; +import { TestFailure, TestMessage, TestMessageStackFrame } from '../test-service'; import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; - +import { URI } from '@theia/core/lib/common/uri'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { NavigationLocationService } from '@theia/editor/lib/browser/navigation/navigation-location-service'; +import { NavigationLocation, Position } from '@theia/editor/lib/browser/navigation/navigation-location'; @injectable() export class TestResultWidget extends BaseWidget { @@ -29,6 +32,10 @@ export class TestResultWidget extends BaseWidget { @inject(TestOutputUIModel) uiModel: TestOutputUIModel; @inject(MarkdownRenderer) markdownRenderer: MarkdownRenderer; + @inject(OpenerService) openerService: OpenerService; + @inject(FileService) fileService: FileService; + @inject(NavigationLocationService) navigationService: NavigationLocationService; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; protected toDisposeOnRender = new DisposableCollection(); protected input: TestMessage[] = []; @@ -36,6 +43,7 @@ export class TestResultWidget extends BaseWidget { constructor() { super(); + this.addClass('theia-test-result-view'); this.id = TestResultWidget.ID; this.title.label = nls.localizeByDefault('Test Results'); this.title.caption = nls.localizeByDefault('Test Results'); @@ -83,6 +91,48 @@ export class TestResultWidget extends BaseWidget { } else { this.content.append(this.node.ownerDocument.createTextNode(message.message)); } + if (message.stackTrace) { + const stackTraceElement = this.node.ownerDocument.createElement('div'); + message.stackTrace.map(frame => this.renderFrame(frame, stackTraceElement)); + this.content.append(stackTraceElement); + } + }); + } + + renderFrame(stackFrame: TestMessageStackFrame, stackTraceElement: HTMLElement): void { + const frameElement = stackTraceElement.ownerDocument.createElement('div'); + frameElement.classList.add('debug-frame'); + frameElement.append(` ${nls.localize('theia/test/stackFrameAt', 'at')} ${stackFrame.label}`); + + // Add URI information as clickable links + if (stackFrame.uri) { + frameElement.append(' ('); + const uri = new URI(stackFrame.uri); + + const link = this.node.ownerDocument.createElement('a'); + let content = `${this.labelProvider.getName(uri)}`; + if (stackFrame.position) { + // Display Position as a 1-based position, similar to Monaco ones. + const monacoPosition = { + lineNumber: stackFrame.position.line + 1, + column: stackFrame.position.character + 1 + }; + content += `:${monacoPosition.lineNumber}:${monacoPosition.column}`; + } + link.textContent = content; + link.href = `${uri}`; + link.onclick = () => this.openUriInWorkspace(uri, stackFrame.position); + frameElement.append(link); + frameElement.append(')'); + } + stackTraceElement.append(frameElement); + } + + async openUriInWorkspace(uri: URI, position?: Position): Promise { + this.fileService.resolve(uri).then(stat => { + if (stat.isFile) { + this.navigationService.reveal(NavigationLocation.create(uri, position ?? { line: 0, character: 0 })); + } }); } diff --git a/packages/test/src/browser/view/test-run-widget.tsx b/packages/test/src/browser/view/test-run-widget.tsx index 296573ffb63e9..02001bd4347cf 100644 --- a/packages/test/src/browser/view/test-run-widget.tsx +++ b/packages/test/src/browser/view/test-run-widget.tsx @@ -20,7 +20,7 @@ import { ContextMenuRenderer, codicon } from '@theia/core/lib/browser'; import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; -import { TestController, TestExecutionState, TestItem, TestOutputItem, TestRun, TestService } from '../test-service'; +import { TestController, TestExecutionState, TestFailure, TestItem, TestMessage, TestOutputItem, TestRun, TestService } from '../test-service'; import * as React from '@theia/core/shared/react'; import { Disposable, DisposableCollection, Event, nls } from '@theia/core'; import { TestExecutionStateManager } from './test-execution-state-manager'; @@ -198,7 +198,7 @@ export class TestRunTreeWidget extends TreeWidget { @postConstruct() protected override init(): void { super.init(); - this.addClass('theia-test-result-view'); + this.addClass('theia-test-run-view'); this.model.onSelectionChanged(() => { const node = this.model.selectedNodes[0]; if (node instanceof TestRunNode) { @@ -251,11 +251,16 @@ export class TestRunTreeWidget extends TreeWidget { } } - protected override toContextMenuArgs(node: SelectableTreeNode): (TestRun | TestItem)[] { + protected override toContextMenuArgs(node: SelectableTreeNode): (TestRun | TestItem | TestMessage[])[] { if (node instanceof TestRunNode) { return [node.run]; } else if (node instanceof TestItemNode) { - return [node.item]; + const item = node.item; + const executionState = node.parent.run.getTestState(node.item); + if (TestFailure.is(executionState)) { + return [item, executionState.messages]; + } + return [item]; } return []; } diff --git a/packages/test/src/browser/view/test-view-contribution.ts b/packages/test/src/browser/view/test-view-contribution.ts index cdff424f81510..37f123be2590d 100644 --- a/packages/test/src/browser/view/test-view-contribution.ts +++ b/packages/test/src/browser/view/test-view-contribution.ts @@ -26,6 +26,7 @@ import { NavigationLocationService } from '@theia/editor/lib/browser/navigation/ import { NavigationLocation } from '@theia/editor/lib/browser/navigation/navigation-location'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileNavigatorCommands } from '@theia/navigator/lib/browser/file-navigator-commands'; +export const PLUGIN_TEST_VIEW_TITLE_MENU = ['plugin_test', 'title']; export namespace TestViewCommands { /** @@ -109,6 +110,12 @@ export namespace TestViewCommands { category: 'Test' }); + export const SELECT_DEFAULT_PROFILES: Command = Command.toDefaultLocalizedCommand({ + id: TestCommandId.SelectDefaultTestProfiles, + label: 'Select Default Test Profiles...', + category: 'Test' + }); + export const CLEAR_ALL_RESULTS: Command = Command.toDefaultLocalizedCommand({ id: TestCommandId.ClearTestResultsAction, label: 'Clear All Results', @@ -186,6 +193,14 @@ export class TestViewContribution extends AbstractViewContribution TestItem.is(t), + isVisible: t => TestItem.is(t), + execute: () => { + this.testService.selectDefaultProfile(); + } + }); + commands.registerCommand(TestViewCommands.DEBUG_TEST, { isEnabled: t => TestItem.is(t), isVisible: t => TestItem.is(t), @@ -254,6 +269,11 @@ export class TestViewContribution extends AbstractViewContribution { - + bindTestPreferences(bind); bindContributionProvider(bind, TestContribution); + bind(TestContextKeyService).toSelf().inSingletonScope(); bind(TestService).to(DefaultTestService).inSingletonScope(); bind(WidgetFactory).toDynamicValue(({ container }) => ({ @@ -103,7 +107,7 @@ export default new ContainerModule(bind => { bind(TabBarToolbarContribution).toService(TestRunViewContribution); bind(TestExecutionStateManager).toSelf().inSingletonScope(); bind(TestOutputUIModel).toSelf().inSingletonScope(); - + bind(TestExecutionProgressService).to(DefaultTestExecutionProgressService).inSingletonScope(); }); export function createTestTreeContainer(parent: interfaces.Container): Container { diff --git a/packages/timeline/package.json b/packages/timeline/package.json index b71766dda3e67..f778a8d5978f9 100644 --- a/packages/timeline/package.json +++ b/packages/timeline/package.json @@ -1,10 +1,11 @@ { "name": "@theia/timeline", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Timeline Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/navigator": "1.44.0" + "@theia/core": "1.54.0", + "@theia/navigator": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -39,7 +40,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/toolbar/package.json b/packages/toolbar/package.json index b042d2b09939d..7c0abda7c4399 100644 --- a/packages/toolbar/package.json +++ b/packages/toolbar/package.json @@ -1,6 +1,6 @@ { "name": "@theia/toolbar", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Toolbar", "keywords": [ "theia-extension" @@ -27,18 +27,19 @@ "watch": "theiaext watch" }, "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "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/search-in-workspace": "1.44.0", - "@theia/userstorage": "1.44.0", - "@theia/workspace": "1.44.0", + "@theia/core": "1.54.0", + "@theia/editor": "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/search-in-workspace": "1.54.0", + "@theia/userstorage": "1.54.0", + "@theia/workspace": "1.54.0", "ajv": "^6.5.3", "jsonc-parser": "^2.2.0", - "perfect-scrollbar": "^1.3.0" + "perfect-scrollbar": "^1.3.0", + "tslib": "^2.6.2" }, "theiaExtensions": [ { diff --git a/packages/toolbar/src/browser/toolbar-controller.ts b/packages/toolbar/src/browser/toolbar-controller.ts index 0198b2a5cf199..16b320c22a9d7 100644 --- a/packages/toolbar/src/browser/toolbar-controller.ts +++ b/packages/toolbar/src/browser/toolbar-controller.ts @@ -17,7 +17,6 @@ import { Command, ContributionProvider, Emitter, MaybePromise, MessageService } from '@theia/core'; import { Widget } from '@theia/core/lib/browser'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -import { TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify'; import { ToolbarDefaultsFactory } from './toolbar-defaults'; @@ -76,7 +75,7 @@ export class ToolbarController { if (contribution) { newGroup.push(contribution); } - } else if (TabBarToolbarItem.is(item)) { + } else { newGroup.push({ ...item }); } } diff --git a/packages/toolbar/src/browser/toolbar-interfaces.ts b/packages/toolbar/src/browser/toolbar-interfaces.ts index b43ed7989c300..1d1f0776c64ed 100644 --- a/packages/toolbar/src/browser/toolbar-interfaces.ts +++ b/packages/toolbar/src/browser/toolbar-interfaces.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { interfaces } from '@theia/core/shared/inversify'; -import { ReactTabBarToolbarItem, TabBarToolbar, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { ReactTabBarToolbarItem, RenderedToolbarItem, TabBarToolbar, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; export enum ToolbarAlignment { LEFT = 'left', @@ -52,7 +52,7 @@ export const Toolbar = Symbol('Toolbar'); export const ToolbarFactory = Symbol('ToolbarFactory'); export type Toolbar = TabBarToolbar; -export type ToolbarItem = ToolbarContribution | TabBarToolbarItem; +export type ToolbarItem = ToolbarContribution | RenderedToolbarItem; export interface DeflatedContributedToolbarItem { id: string; group: 'contributed' }; export type ToolbarItemDeflated = DeflatedContributedToolbarItem | TabBarToolbarItem; diff --git a/packages/toolbar/src/browser/toolbar.tsx b/packages/toolbar/src/browser/toolbar.tsx index d52a725a511e4..7c4380386e596 100644 --- a/packages/toolbar/src/browser/toolbar.tsx +++ b/packages/toolbar/src/browser/toolbar.tsx @@ -17,7 +17,7 @@ import * as React from '@theia/core/shared/react'; import { Anchor, ContextMenuAccess, KeybindingRegistry, PreferenceService, Widget, WidgetManager } from '@theia/core/lib/browser'; import { LabelIcon } from '@theia/core/lib/browser/label-parser'; -import { TabBarToolbar, TabBarToolbarFactory, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { ReactTabBarToolbarItem, RenderedToolbarItem, TabBarToolbar, TabBarToolbarFactory } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { MenuPath, ProgressService } from '@theia/core'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; @@ -239,7 +239,7 @@ export class ToolbarImpl extends TabBarToolbar { let toolbarItemClassNames = ''; let renderBody: React.ReactNode; - if (TabBarToolbarItem.is(item)) { + if (!ReactTabBarToolbarItem.is(item)) { toolbarItemClassNames = TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM; if (this.evaluateWhenClause(item.when)) { toolbarItemClassNames += ' enabled'; @@ -265,7 +265,7 @@ export class ToolbarImpl extends TabBarToolbar { onMouseOut={this.onMouseUpEvent} draggable={true} onDragStart={this.handleOnDragStart} - onClick={this.executeCommand} + onClick={e => this.executeCommand(e, item)} onDragOver={this.handleOnDragEnter} onDragLeave={this.handleOnDragLeave} onContextMenu={this.handleContextMenu} @@ -279,7 +279,7 @@ export class ToolbarImpl extends TabBarToolbar { } protected override renderItem( - item: TabBarToolbarItem, + item: RenderedToolbarItem, ): React.ReactNode { const classNames = []; if (item.text) { @@ -290,7 +290,7 @@ export class ToolbarImpl extends TabBarToolbar { } } } - const command = this.commands.getCommand(item.command); + const command = this.commands.getCommand(item.command!); const iconClass = (typeof item.icon === 'function' && item.icon()) || item.icon || command?.iconClass; if (iconClass) { classNames.push(iconClass); diff --git a/packages/typehierarchy/package.json b/packages/typehierarchy/package.json index 2b15241846163..0802b6ccd5530 100644 --- a/packages/typehierarchy/package.json +++ b/packages/typehierarchy/package.json @@ -1,12 +1,11 @@ { "name": "@theia/typehierarchy", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Type Hierarchy Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/editor": "1.44.0", - "@types/uuid": "^7.0.3", - "uuid": "^8.0.0" + "@theia/core": "1.54.0", + "@theia/editor": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -41,7 +40,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/typehierarchy/src/browser/tree/typehierarchy-tree.ts b/packages/typehierarchy/src/browser/tree/typehierarchy-tree.ts index 517fe17ebc3e4..65aaaa016e965 100644 --- a/packages/typehierarchy/src/browser/tree/typehierarchy-tree.ts +++ b/packages/typehierarchy/src/browser/tree/typehierarchy-tree.ts @@ -17,7 +17,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { injectable } from '@theia/core/shared/inversify'; -import { v4 } from 'uuid'; +import { generateUuid } from '@theia/core/lib/common/uuid'; import URI from '@theia/core/lib/common/uri'; import { Location } from '@theia/editor/lib/browser/editor'; import { TreeDecoration, DecoratedTreeNode } from '@theia/core/lib/browser/tree/tree-decorator'; @@ -134,7 +134,7 @@ export namespace TypeHierarchyTree { resolved = true; } const node = { - id: v4(), + id: generateUuid(), name: item.name, description: item.detail, parent: undefined, diff --git a/packages/userstorage/package.json b/packages/userstorage/package.json index 128faf347d47e..f43f417ae21e4 100644 --- a/packages/userstorage/package.json +++ b/packages/userstorage/package.json @@ -1,10 +1,11 @@ { "name": "@theia/userstorage", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - User Storage Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/filesystem": "1.44.0" + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -39,7 +40,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/variable-resolver/package.json b/packages/variable-resolver/package.json index 212107c8affcf..5cdb301cebdf6 100644 --- a/packages/variable-resolver/package.json +++ b/packages/variable-resolver/package.json @@ -1,9 +1,10 @@ { "name": "@theia/variable-resolver", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Variable Resolver Extension", "dependencies": { - "@theia/core": "1.44.0" + "@theia/core": "1.54.0", + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -44,7 +45,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/vsx-registry/package.json b/packages/vsx-registry/package.json index 19fd8269872af..3eabc882978ee 100644 --- a/packages/vsx-registry/package.json +++ b/packages/vsx-registry/package.json @@ -1,19 +1,21 @@ { "name": "@theia/vsx-registry", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - VSX Registry", "dependencies": { - "@theia/core": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/ovsx-client": "1.44.0", - "@theia/plugin-ext": "1.44.0", - "@theia/plugin-ext-vscode": "1.44.0", - "@theia/preferences": "1.44.0", - "@theia/workspace": "1.44.0", + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/navigator": "1.54.0", + "@theia/ovsx-client": "1.54.0", + "@theia/plugin-ext": "1.54.0", + "@theia/plugin-ext-vscode": "1.54.0", + "@theia/preferences": "1.54.0", + "@theia/workspace": "1.54.0", + "limiter": "^2.1.0", "luxon": "^2.4.0", "p-debounce": "^2.1.0", "semver": "^7.5.4", - "uuid": "^8.0.0" + "tslib": "^2.6.2" }, "publishConfig": { "access": "public" @@ -53,7 +55,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0", + "@theia/ext-scripts": "1.54.0", "@types/luxon": "^2.3.2" }, "nyc": { diff --git a/packages/vsx-registry/src/browser/style/index.css b/packages/vsx-registry/src/browser/style/index.css index 70c619d240e33..ef05e93adaec3 100644 --- a/packages/vsx-registry/src/browser/style/index.css +++ b/packages/vsx-registry/src/browser/style/index.css @@ -21,6 +21,56 @@ ); } +.vsx-search-container { + display: flex; + align-items: center; + width: 100%; + background: var(--theia-input-background); + border-style: solid; + border-width: var(--theia-border-width); + border-color: var(--theia-input-background); + border-radius: 2px; +} + +.vsx-search-container:focus-within { + border-color: var(--theia-focusBorder); +} + +.vsx-search-container .option-buttons { + height: 23px; + display: flex; + align-items: center; + align-self: flex-start; + background-color: none; + margin: 2px; +} + +.vsx-search-container .option { + width: 21px; + height: 21px; + margin: 0 1px; + display: inline-block; + box-sizing: border-box; + align-items: center; + user-select: none; + background-repeat: no-repeat; + background-position: center; + border: var(--theia-border-width) solid transparent; + opacity: 0.7; + cursor: pointer; +} + +.vsx-search-container .option.enabled { + color: var(--theia-inputOption-activeForeground); + border: var(--theia-border-width) var(--theia-inputOption-activeBorder) solid; + background-color: var(--theia-inputOption-activeBackground); + opacity: 1; +} + +.vsx-search-container .option:hover { + opacity: 1; +} + .theia-vsx-extensions { height: 100%; } @@ -42,10 +92,14 @@ overflow: hidden; line-height: var(--theia-content-line-height); flex: 1; - padding-top: calc(var(--theia-ui-padding) / 2); - padding-bottom: calc(var(--theia-ui-padding) / 2); + margin-top: calc(var(--theia-ui-padding) / 2); + margin-bottom: calc(var(--theia-ui-padding) / 2); } +.theia-vsx-extensions-search-bar .theia-input:focus { + border: none; + outline: none; +} .theia-vsx-extension { display: flex; flex-direction: row; @@ -136,6 +190,15 @@ white-space: nowrap; } +.theia-vsx-extension-action-bar .codicon-verified-filled { + color: var(--theia-extensionIcon-verifiedForeground); + margin-right: 2px; +} + +.theia-vsx-extension-publisher-container { + display: flex; +} + .theia-vsx-extension-action-bar .action { font-size: 90%; min-width: auto !important; diff --git a/packages/vsx-registry/src/browser/vsx-extension-argument-processor.ts b/packages/vsx-registry/src/browser/vsx-extension-argument-processor.ts new file mode 100644 index 0000000000000..e7481a7b07a52 --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extension-argument-processor.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// 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 { injectable } from '@theia/core/shared/inversify'; +import { ArgumentProcessor } from '@theia/plugin-ext/lib/common/commands'; +import { VSXExtension } from './vsx-extension'; + +@injectable() +export class VsxExtensionArgumentProcessor implements ArgumentProcessor { + + processArgument(arg: unknown): unknown { + if (arg instanceof VSXExtension) { + return arg.id; + } + + return arg; + } + +} diff --git a/packages/vsx-registry/src/browser/vsx-extension-commands.ts b/packages/vsx-registry/src/browser/vsx-extension-commands.ts index e79ba161d5ed3..091563201bca9 100644 --- a/packages/vsx-registry/src/browser/vsx-extension-commands.ts +++ b/packages/vsx-registry/src/browser/vsx-extension-commands.ts @@ -36,6 +36,11 @@ export namespace VSXExtensionsCommands { label: nls.localizeByDefault('Install from VSIX') + '...', dialogLabel: nls.localizeByDefault('Install from VSIX') }; + export const INSTALL_VSIX_FILE: Command = Command.toDefaultLocalizedCommand({ + id: 'vsxExtensions.installVSIX', + label: 'Install Extension VSIX', + category: EXTENSIONS_CATEGORY, + }); export const INSTALL_ANOTHER_VERSION: Command = { id: 'vsxExtensions.installAnotherVersion' }; diff --git a/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts b/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts index b715271fa13c6..9bc33c16fafe0 100644 --- a/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts +++ b/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts @@ -36,7 +36,7 @@ export class VSXExtensionEditorManager extends WidgetOpenHandler = new Set([ @@ -75,6 +77,7 @@ export class VSXExtensionData { 'license', 'readme', 'preview', + 'verified', 'namespaceAccess', 'publishedBy' ]); @@ -143,7 +146,7 @@ export class VSXExtension implements VSXExtensionData, TreeElement { } get uri(): URI { - return VSCodeExtensionUri.toUri(this.id); + return VSCodeExtensionUri.fromId(this.id); } get id(): string { @@ -265,6 +268,10 @@ export class VSXExtension implements VSXExtensionData, TreeElement { return this.getData('preview'); } + get verified(): boolean | undefined { + return this.getData('verified'); + } + get namespaceAccess(): VSXExtensionNamespaceAccess | undefined { return this.getData('namespaceAccess'); } @@ -297,13 +304,16 @@ export class VSXExtension implements VSXExtensionData, TreeElement { } async install(options?: PluginDeployOptions): Promise { - this._busy++; - try { - await this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () => - this.pluginServer.deploy(this.uri.toString(), undefined, options) - ); - } finally { - this._busy--; + if (!this.verified) { + const choice = await new ConfirmDialog({ + title: nls.localize('theia/vsx-registry/confirmDialogTitle', 'Are you sure you want to proceed with the installation ?'), + msg: nls.localize('theia/vsx-registry/confirmDialogMessage', 'The extension "{0}" is unverified and might pose a security risk.', this.displayName) + }).open(); + if (choice) { + this.doInstall(options); + } + } else { + this.doInstall(options); } } @@ -322,6 +332,17 @@ export class VSXExtension implements VSXExtensionData, TreeElement { } } + protected async doInstall(options?: PluginDeployOptions): Promise { + this._busy++; + try { + await this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () => + this.pluginServer.deploy(this.uri.toString(), undefined, options) + ); + } finally { + this._busy--; + } + } + handleContextMenu(e: React.MouseEvent): void { e.preventDefault(); this.contextMenuRenderer.render({ @@ -431,7 +452,7 @@ export abstract class AbstractVSXExtensionComponent { outOfSynch - ? + ? : } @@ -464,7 +485,7 @@ export namespace VSXExtensionComponent { export class VSXExtensionComponent extends AbstractVSXExtensionComponent { override render(): React.ReactNode { - const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltip } = this.props.extension; + const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltip, verified } = this.props.extension; return
    { + if (event.button === 2) { + this.manage(event); + } + }} > {iconUrl ? : @@ -491,7 +517,16 @@ export class VSXExtensionComponent
    {description}
    - {publisher} +
    + {verified === true ? ( + + ) : verified === false ? ( + + ) : ( + + )} + {publisher} +
    {this.renderAction(this.props.host)}
    diff --git a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts index 9de6fe277faea..f29d92a566a19 100644 --- a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts +++ b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts @@ -14,29 +14,33 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { DateTime } from 'luxon'; -import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; -import debounce = require('@theia/core/shared/lodash.debounce'); -import { Command, CommandRegistry } from '@theia/core/lib/common/command'; -import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; -import { VSXExtensionsViewContainer } from './vsx-extensions-view-container'; -import { VSXExtensionsModel } from './vsx-extensions-model'; +import { CommonMenus, LabelProvider, PreferenceService, QuickInputService, QuickPickItem } from '@theia/core/lib/browser'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; -import { Color } from '@theia/core/lib/common/color'; import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; -import { MenuModelRegistry, MessageService, nls } from '@theia/core/lib/common'; +import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; +import { CompoundMenuNodeRole, MenuModelRegistry, MessageService, SelectionService, nls } from '@theia/core/lib/common'; +import { Color } from '@theia/core/lib/common/color'; +import { Command, CommandRegistry } from '@theia/core/lib/common/command'; +import URI from '@theia/core/lib/common/uri'; +import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { FileDialogService, OpenFileDialogProps } from '@theia/filesystem/lib/browser'; -import { LabelProvider, PreferenceService, QuickPickItem, QuickInputService, CommonMenus } from '@theia/core/lib/browser'; +import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution'; +import { OVSXApiFilterProvider, VSXExtensionRaw } from '@theia/ovsx-client'; import { VscodeCommands } from '@theia/plugin-ext-vscode/lib/browser/plugin-vscode-commands-contribution'; -import { VSXExtensionsContextMenu, VSXExtension } from './vsx-extension'; -import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; -import { BUILTIN_QUERY, INSTALLED_QUERY, RECOMMENDED_QUERY } from './vsx-extensions-search-model'; +import { DateTime } from 'luxon'; +import { OVSXClientProvider } from '../common/ovsx-client-provider'; import { IGNORE_RECOMMENDATIONS_ID } from './recommended-extensions/recommended-extensions-preference-contribution'; +import { VSXExtension, VSXExtensionsContextMenu } from './vsx-extension'; import { VSXExtensionsCommands } from './vsx-extension-commands'; -import { VSXExtensionRaw, OVSXApiFilter } from '@theia/ovsx-client'; -import { OVSXClientProvider } from '../common/ovsx-client-provider'; +import { VSXExtensionsModel } from './vsx-extensions-model'; +import { BUILTIN_QUERY, INSTALLED_QUERY, RECOMMENDED_QUERY } from './vsx-extensions-search-model'; +import { VSXExtensionsViewContainer } from './vsx-extensions-view-container'; +import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; +import debounce = require('@theia/core/shared/lodash.debounce'); export namespace VSXCommands { export const TOGGLE_EXTENSIONS: Command = { @@ -55,8 +59,10 @@ export class VSXExtensionsContribution extends AbstractViewContribution this.installFromVSIX() }); + commands.registerCommand(VSXExtensionsCommands.INSTALL_VSIX_FILE, + UriAwareCommandHandler.MonoSelect(this.selectionService, { + execute: fileURI => this.installVsixFile(fileURI), + isEnabled: fileURI => fileURI.scheme === 'file' && fileURI.path.ext === '.vsix' + }) + ); + commands.registerCommand(VSXExtensionsCommands.INSTALL_ANOTHER_VERSION, { // Check downloadUrl to ensure we have an idea of where to look for other versions. isEnabled: (extension: VSXExtension) => !extension.builtin && !!extension.downloadUrl, @@ -143,6 +156,15 @@ export class VSXExtensionsContribution extends AbstractViewContribution { + const extensionName = this.labelProvider.getName(fileURI); + try { + await this.commandRegistry.executeCommand(VscodeCommands.INSTALL_FROM_VSIX.id, fileURI); + this.messageService.info(nls.localizeByDefault('Completed installing {0} extension from VSIX.', extensionName)); + } catch (e) { + this.messageService.error(nls.localize('theia/vsx-registry/failedInstallingVSIX', 'Failed to install {0} from VSIX.', extensionName)); + console.warn(e); + } + } + /** * Given an extension, displays a quick pick of other compatible versions and installs the selected version. * @@ -221,8 +261,14 @@ export class VSXExtensionsContribution extends AbstractViewContribution { return this.doChange(async () => { - const searchResult = new Set(); + this._searchResult = new Set(); if (!param.query) { - this._searchResult = searchResult; return; } const client = await this.clientProvider(); - const result = await client.search(param); - this._searchError = result.error; - if (token.isCancellationRequested) { - return; - } - for (const data of result.extensions) { - const id = data.namespace.toLowerCase() + '.' + data.name.toLowerCase(); - const allVersions = this.vsxApiFilter.getLatestCompatibleVersion(data); - if (!allVersions) { - continue; + const filter = await this.vsxApiFilter(); + try { + const result = await client.search(param); + + if (token.isCancellationRequested) { + return; + } + for (const data of result.extensions) { + const id = data.namespace.toLowerCase() + '.' + data.name.toLowerCase(); + const allVersions = filter.getLatestCompatibleVersion(data); + if (!allVersions) { + continue; + } + if (this.preferences.get('extensions.onlyShowVerifiedExtensions')) { + this.fetchVerifiedStatus(id, client, allVersions).then(verified => { + this.doChange(() => { + this.addExtensions(data, id, allVersions, !!verified); + return Promise.resolve(); + }); + }); + } else { + this.addExtensions(data, id, allVersions); + this.fetchVerifiedStatus(id, client, allVersions).then(verified => { + this.doChange(() => { + let extension = this.getExtension(id); + extension = this.setExtension(id); + extension.update(Object.assign({ + verified: verified + })); + return Promise.resolve(); + }); + }); + } } - this.setExtension(id).update(Object.assign(data, { - publisher: data.namespace, - downloadUrl: data.files.download, - iconUrl: data.files.icon, - readmeUrl: data.files.readme, - licenseUrl: data.files.license, - version: allVersions.version - })); - searchResult.add(id); + } catch (error) { + this._searchError = error?.message || String(error); } - this._searchResult = searchResult; + }, token); } + protected async fetchVerifiedStatus(id: string, client: OVSXClient, allVersions: VSXAllVersions): Promise { + try { + const res = await client.query({ extensionId: id, extensionVersion: allVersions.version, includeAllVersions: true }); + const extension = res.extensions?.[0]; + let verified = extension?.verified; + if (!verified && extension?.publishedBy.loginName === 'open-vsx') { + verified = true; + } + return verified; + } catch (error) { + console.error(error); + return false; + } + } + + protected addExtensions(data: VSXSearchEntry, id: string, allVersions: VSXAllVersions, verified?: boolean): void { + if (!this.preferences.get('extensions.onlyShowVerifiedExtensions') || verified) { + const extension = this.setExtension(id); + extension.update(Object.assign(data, { + publisher: data.namespace, + downloadUrl: data.files.download, + iconUrl: data.files.icon, + readmeUrl: data.files.readme, + licenseUrl: data.files.license, + version: allVersions.version, + verified: verified + })); + this._searchResult.add(id); + } + } + protected async updateInstalled(): Promise { const prevInstalled = this._installed; return this.doChange(async () => { @@ -312,18 +369,22 @@ export class VSXExtensionsModel { if (!this.shouldRefresh(extension)) { return extension; } - const client = await this.clientProvider(); + const filter = await this.vsxApiFilter(); + const targetPlatform = await this.applicationServer.getApplicationPlatform() as VSXTargetPlatform; let data: VSXExtensionRaw | undefined; if (version === undefined) { - const { extensions } = await client.query({ extensionId: id, includeAllVersions: true }); - if (extensions?.length) { - data = this.vsxApiFilter.getLatestCompatibleExtension(extensions); - } + data = await filter.findLatestCompatibleExtension({ + extensionId: id, + includeAllVersions: true, + targetPlatform + }); } else { - const { extensions } = await client.query({ extensionId: id, extensionVersion: version, includeAllVersions: true }); - if (extensions?.length) { - data = extensions?.[0]; - } + data = await filter.findLatestCompatibleExtension({ + extensionId: id, + extensionVersion: version, + includeAllVersions: true, + targetPlatform + }); } if (!data) { return; @@ -331,6 +392,11 @@ export class VSXExtensionsModel { if (data.error) { return this.onDidFailRefresh(id, data.error); } + if (!data.verified) { + if (data.publishedBy.loginName === 'open-vsx') { + data.verified = true; + } + } extension = this.setExtension(id); extension.update(Object.assign(data, { publisher: data.namespace, @@ -338,7 +404,8 @@ export class VSXExtensionsModel { iconUrl: data.files.icon, readmeUrl: data.files.readme, licenseUrl: data.files.license, - version: data.version + version: data.version, + verified: data.verified })); return extension; } catch (e) { diff --git a/packages/vsx-registry/src/browser/vsx-extensions-preferences.ts b/packages/vsx-registry/src/browser/vsx-extensions-preferences.ts new file mode 100644 index 0000000000000..ba5406aa1c3e9 --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extensions-preferences.ts @@ -0,0 +1,58 @@ +// ***************************************************************************** +// Copyright (C) 2023 Ericsson 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 { interfaces } from '@theia/core/shared/inversify'; +import { + createPreferenceProxy, + PreferenceProxy, + PreferenceService, + PreferenceSchema, + PreferenceContribution +} from '@theia/core/lib/browser/preferences'; +import { nls } from '@theia/core'; + +export const VsxExtensionsPreferenceSchema: PreferenceSchema = { + 'type': 'object', + properties: { + 'extensions.onlyShowVerifiedExtensions': { + type: 'boolean', + default: false, + description: nls.localize('theia/vsx-registry/onlyShowVerifiedExtensionsDescription', 'This allows the {0} to only show verified extensions.', 'Open VSX Registry') + }, + } +}; + +export interface VsxExtensionsConfiguration { + 'extensions.onlyShowVerifiedExtensions': boolean; +} + +export const VsxExtensionsPreferenceContribution = Symbol('VsxExtensionsPreferenceContribution'); +export const VsxExtensionsPreferences = Symbol('VsxExtensionsPreferences'); +export type VsxExtensionsPreferences = PreferenceProxy; + +export function createVsxExtensionsPreferences(preferences: PreferenceService, schema: PreferenceSchema = VsxExtensionsPreferenceSchema): VsxExtensionsPreferences { + return createPreferenceProxy(preferences, schema); +} + +export function bindVsxExtensionsPreferences(bind: interfaces.Bind): void { + bind(VsxExtensionsPreferences).toDynamicValue(ctx => { + const preferences = ctx.container.get(PreferenceService); + const contribution = ctx.container.get(VsxExtensionsPreferenceContribution); + return createVsxExtensionsPreferences(preferences, contribution.schema); + }).inSingletonScope(); + bind(VsxExtensionsPreferenceContribution).toConstantValue({ schema: VsxExtensionsPreferenceSchema }); + bind(PreferenceContribution).toService(VsxExtensionsPreferenceContribution); +} diff --git a/packages/vsx-registry/src/browser/vsx-extensions-search-bar.tsx b/packages/vsx-registry/src/browser/vsx-extensions-search-bar.tsx index 240a480d862df..1702a41cab1f8 100644 --- a/packages/vsx-registry/src/browser/vsx-extensions-search-bar.tsx +++ b/packages/vsx-registry/src/browser/vsx-extensions-search-bar.tsx @@ -16,37 +16,57 @@ import * as React from '@theia/core/shared/react'; import { injectable, postConstruct, inject } from '@theia/core/shared/inversify'; -import { ReactWidget, Message } from '@theia/core/lib/browser/widgets'; +import { ReactWidget, Message, codicon } from '@theia/core/lib/browser/widgets'; +import { PreferenceService } from '@theia/core/lib/browser'; import { VSXExtensionsSearchModel } from './vsx-extensions-search-model'; +import { VSXExtensionsModel } from './vsx-extensions-model'; import { nls } from '@theia/core/lib/common/nls'; @injectable() export class VSXExtensionsSearchBar extends ReactWidget { + @inject(VSXExtensionsModel) + protected readonly extensionsModel: VSXExtensionsModel; + @inject(VSXExtensionsSearchModel) - protected readonly model: VSXExtensionsSearchModel; + protected readonly searchModel: VSXExtensionsSearchModel; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + protected input: HTMLInputElement | undefined; + protected onlyShowVerifiedExtensions: boolean | undefined; @postConstruct() protected init(): void { + this.onlyShowVerifiedExtensions = this.preferenceService.get('extensions.onlyShowVerifiedExtensions'); this.id = 'vsx-extensions-search-bar'; this.addClass('theia-vsx-extensions-search-bar'); - this.model.onDidChangeQuery((query: string) => this.updateSearchTerm(query)); + this.searchModel.onDidChangeQuery((query: string) => this.updateSearchTerm(query)); + this.preferenceService.onPreferenceChanged(change => { + if (change.preferenceName === 'extensions.onlyShowVerifiedExtensions') { + this.extensionsModel.setOnlyShowVerifiedExtensions(!!change.newValue); + this.onlyShowVerifiedExtensions = change.newValue; + this.update(); + } + }); } - protected input: HTMLInputElement | undefined; - protected render(): React.ReactNode { - return this.input = input || undefined} - defaultValue={this.model.query} - spellCheck={false} - className='theia-input' - placeholder={nls.localize('theia/vsx-registry/searchPlaceholder', 'Search Extensions in {0}', 'Open VSX Registry')} - onChange={this.updateQuery}> - ; + return
    + this.input = input || undefined} + defaultValue={this.searchModel.query} + spellCheck={false} + className='theia-input' + placeholder={nls.localize('theia/vsx-registry/searchPlaceholder', 'Search Extensions in {0}', 'Open VSX Registry')} + onChange={this.updateQuery}> + + {this.renderOptionContainer()} +
    ; } - protected updateQuery = (e: React.ChangeEvent) => this.model.query = e.target.value; + protected updateQuery = (e: React.ChangeEvent) => this.searchModel.query = e.target.value; protected updateSearchTerm(term: string): void { if (this.input) { @@ -54,6 +74,24 @@ export class VSXExtensionsSearchBar extends ReactWidget { } } + protected renderOptionContainer(): React.ReactNode { + const showVerifiedExtensions = this.renderShowVerifiedExtensions(); + return
    {showVerifiedExtensions}
    ; + } + + protected renderShowVerifiedExtensions(): React.ReactNode { + return this.handleShowVerifiedExtensionsClick()}> + ; + } + + protected handleShowVerifiedExtensionsClick(): void { + this.extensionsModel.setOnlyShowVerifiedExtensions(!this.onlyShowVerifiedExtensions); + this.update(); + } + protected override onActivateRequest(msg: Message): void { super.onActivateRequest(msg); if (this.input) { diff --git a/packages/vsx-registry/src/browser/vsx-extensions-widget.tsx b/packages/vsx-registry/src/browser/vsx-extensions-widget.tsx index 6b79ed3dea49f..63e2a38579cc7 100644 --- a/packages/vsx-registry/src/browser/vsx-extensions-widget.tsx +++ b/packages/vsx-registry/src/browser/vsx-extensions-widget.tsx @@ -15,7 +15,7 @@ // ***************************************************************************** import { injectable, interfaces, postConstruct, inject } from '@theia/core/shared/inversify'; -import { TreeModel, TreeNode } from '@theia/core/lib/browser'; +import { Message, TreeModel, TreeNode } from '@theia/core/lib/browser'; import { SourceTreeWidget } from '@theia/core/lib/browser/source-tree'; import { VSXExtensionsSource, VSXExtensionsSourceOptions } from './vsx-extensions-source'; import { nls } from '@theia/core/lib/common/nls'; @@ -153,4 +153,13 @@ export class VSXExtensionsWidget extends SourceTreeWidget implements BadgeWidget } return super.renderTree(model); } + + protected override onAfterShow(msg: Message): void { + super.onAfterShow(msg); + if (this.options.id === VSXExtensionsSourceOptions.INSTALLED) { + // This is needed when an Extension was installed outside of the extension view. + // E.g. using explorer context menu. + this.doUpdateRows(); + } + } } diff --git a/packages/vsx-registry/src/browser/vsx-language-quick-pick-service.ts b/packages/vsx-registry/src/browser/vsx-language-quick-pick-service.ts index 35af7885c4465..717a35d1fe048 100644 --- a/packages/vsx-registry/src/browser/vsx-language-quick-pick-service.ts +++ b/packages/vsx-registry/src/browser/vsx-language-quick-pick-service.ts @@ -42,47 +42,49 @@ export class VSXLanguageQuickPickService extends LanguageQuickPickService { protected override async getAvailableLanguages(): Promise { const client = await this.clientProvider(); - const searchResult = await client.search({ - category: 'Language Packs', - sortBy: 'downloadCount', - sortOrder: 'desc', - size: 20 - }); - if (searchResult.error) { - throw new Error('Error while loading available languages: ' + searchResult.error); - } + try { + const searchResult = await client.search({ + category: 'Language Packs', + sortBy: 'downloadCount', + sortOrder: 'desc', + size: 20 + }); - const extensionLanguages = await Promise.all( - searchResult.extensions.map(async extension => ({ - extension, - languages: await this.loadExtensionLanguages(extension) - })) - ); + const extensionLanguages = await Promise.all( + searchResult.extensions.map(async extension => ({ + extension, + languages: await this.loadExtensionLanguages(extension) + })) + ); - const languages = new Map(); + const languages = new Map(); - for (const extension of extensionLanguages) { - for (const localizationContribution of extension.languages) { - if (!languages.has(localizationContribution.languageId)) { - languages.set(localizationContribution.languageId, { - ...this.createLanguageQuickPickItem(localizationContribution), - execute: async () => { - const progress = await this.messageService.showProgress({ - text: nls.localizeByDefault('Installing {0} language support...', - localizationContribution.localizedLanguageName ?? localizationContribution.languageName ?? localizationContribution.languageId), - }); - try { - const extensionUri = VSCodeExtensionUri.toUri(extension.extension.name, extension.extension.namespace).toString(); - await this.pluginServer.deploy(extensionUri); - } finally { - progress.cancel(); + for (const extension of extensionLanguages) { + for (const localizationContribution of extension.languages) { + if (!languages.has(localizationContribution.languageId)) { + languages.set(localizationContribution.languageId, { + ...this.createLanguageQuickPickItem(localizationContribution), + execute: async () => { + const progress = await this.messageService.showProgress({ + text: nls.localizeByDefault('Installing {0} language support...', + localizationContribution.localizedLanguageName ?? localizationContribution.languageName ?? localizationContribution.languageId), + }); + try { + const extensionUri = VSCodeExtensionUri.fromId(`${extension.extension.namespace}.${extension.extension.name}`).toString(); + await this.pluginServer.deploy(extensionUri); + } finally { + progress.cancel(); + } } - } - }); + }); + } } } + return Array.from(languages.values()); + } catch (error) { + console.error(error); + return []; } - return Array.from(languages.values()); } protected async loadExtensionLanguages(extension: VSXSearchEntry): Promise { diff --git a/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts b/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts index f3ebbccd2e211..19736daf75b62 100644 --- a/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts +++ b/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts @@ -18,7 +18,9 @@ import '../../src/browser/style/index.css'; import { ContainerModule } from '@theia/core/shared/inversify'; import { - WidgetFactory, bindViewContribution, FrontendApplicationContribution, ViewContainerIdentifier, OpenHandler, WidgetManager, WebSocketConnectionProvider + WidgetFactory, bindViewContribution, FrontendApplicationContribution, ViewContainerIdentifier, OpenHandler, WidgetManager, WebSocketConnectionProvider, + WidgetStatusBarContribution, + noopWidgetStatusBarContribution } from '@theia/core/lib/browser'; import { VSXExtensionsViewContainer } from './vsx-extensions-view-container'; import { VSXExtensionsContribution } from './vsx-extensions-contribution'; @@ -33,9 +35,12 @@ import { VSXExtensionsSourceOptions } from './vsx-extensions-source'; import { VSXExtensionsSearchModel } from './vsx-extensions-search-model'; import { bindExtensionPreferences } from './recommended-extensions/recommended-extensions-preference-contribution'; import { bindPreferenceProviderOverrides } from './recommended-extensions/preference-provider-overrides'; +import { bindVsxExtensionsPreferences } from './vsx-extensions-preferences'; import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment'; import { LanguageQuickPickService } from '@theia/core/lib/browser/i18n/language-quick-pick-service'; import { VSXLanguageQuickPickService } from './vsx-language-quick-pick-service'; +import { VsxExtensionArgumentProcessor } from './vsx-extension-argument-processor'; +import { ArgumentProcessorContribution } from '@theia/plugin-ext/lib/main/browser/command-registry-main'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(VSXEnvironment) @@ -61,6 +66,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { })).inSingletonScope(); bind(VSXExtensionEditorManager).toSelf().inSingletonScope(); bind(OpenHandler).toService(VSXExtensionEditorManager); + bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(VSXExtensionEditor)); bind(WidgetFactory).toDynamicValue(({ container }) => ({ id: VSXExtensionsWidget.ID, @@ -75,6 +81,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { progressLocationId: 'extensions' }); child.bind(VSXExtensionsViewContainer).toSelf(); + child.bind(VSXExtensionsSearchBar).toSelf().inSingletonScope(); const viewContainer = child.get(VSXExtensionsViewContainer); const widgetManager = child.get(WidgetManager); for (const id of [ @@ -93,7 +100,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { })).inSingletonScope(); bind(VSXExtensionsSearchModel).toSelf().inSingletonScope(); - bind(VSXExtensionsSearchBar).toSelf().inSingletonScope(); rebind(LanguageQuickPickService).to(VSXLanguageQuickPickService).inSingletonScope(); @@ -103,4 +109,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bindExtensionPreferences(bind); bindPreferenceProviderOverrides(bind, unbind); + bindVsxExtensionsPreferences(bind); + + bind(VsxExtensionArgumentProcessor).toSelf().inSingletonScope(); + bind(ArgumentProcessorContribution).toService(VsxExtensionArgumentProcessor); }); diff --git a/packages/vsx-registry/src/common/vsx-environment.ts b/packages/vsx-registry/src/common/vsx-environment.ts index 4366d5ef56dc7..8b1ef42657bce 100644 --- a/packages/vsx-registry/src/common/vsx-environment.ts +++ b/packages/vsx-registry/src/common/vsx-environment.ts @@ -20,6 +20,7 @@ export const VSX_ENVIRONMENT_PATH = '/services/vsx-environment'; export const VSXEnvironment = Symbol('VSXEnvironment'); export interface VSXEnvironment { + getRateLimit(): Promise; getRegistryUri(): Promise; getRegistryApiUri(): Promise; getVscodeApiVersion(): Promise; diff --git a/packages/vsx-registry/src/common/vsx-registry-common-module.ts b/packages/vsx-registry/src/common/vsx-registry-common-module.ts index 0fd32471a58f7..5a0940c903d9e 100644 --- a/packages/vsx-registry/src/common/vsx-registry-common-module.ts +++ b/packages/vsx-registry/src/common/vsx-registry-common-module.ts @@ -17,8 +17,11 @@ import { ContainerModule } from '@theia/core/shared/inversify'; import { OVSXClientProvider, OVSXUrlResolver } from '../common'; import { RequestService } from '@theia/core/shared/@theia/request'; -import { ExtensionIdMatchesFilterFactory, OVSXApiFilter, OVSXApiFilterImpl, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client'; +import { + ExtensionIdMatchesFilterFactory, OVSXApiFilter, OVSXApiFilterImpl, OVSXApiFilterProvider, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory +} from '@theia/ovsx-client'; import { VSXEnvironment } from './vsx-environment'; +import { RateLimiter } from 'limiter'; export default new ContainerModule(bind => { bind(OVSXUrlResolver) @@ -32,10 +35,15 @@ export default new ContainerModule(bind => { .all([ vsxEnvironment.getRegistryApiUri(), vsxEnvironment.getOvsxRouterConfig?.(), + vsxEnvironment.getRateLimit() ]) - .then(async ([apiUrl, ovsxRouterConfig]) => { + .then(async ([apiUrl, ovsxRouterConfig, rateLimit]) => { + const rateLimiter = new RateLimiter({ + interval: 'second', + tokensPerInterval: rateLimit + }); if (ovsxRouterConfig) { - const clientFactory = OVSXHttpClient.createClientFactory(requestService); + const clientFactory = OVSXHttpClient.createClientFactory(requestService, rateLimiter); return OVSXRouterClient.FromConfig( ovsxRouterConfig, async url => clientFactory(await urlResolver(url)), @@ -44,7 +52,8 @@ export default new ContainerModule(bind => { } return new OVSXHttpClient( await urlResolver(apiUrl), - requestService + requestService, + rateLimiter ); }); // reuse the promise for subsequent calls to this provider @@ -54,10 +63,23 @@ export default new ContainerModule(bind => { bind(OVSXApiFilter) .toDynamicValue(ctx => { const vsxEnvironment = ctx.container.get(VSXEnvironment); - const apiFilter = new OVSXApiFilterImpl('-- temporary invalid version value --'); + const apiFilter = new OVSXApiFilterImpl(undefined!, '-- temporary invalid version value --'); vsxEnvironment.getVscodeApiVersion() .then(apiVersion => apiFilter.supportedApiVersion = apiVersion); + const clientProvider = ctx.container.get(OVSXClientProvider); + Promise.resolve(clientProvider()).then(client => { + apiFilter.client = client; + }); return apiFilter; }) .inSingletonScope(); + bind(OVSXApiFilterProvider) + .toProvider(ctx => async () => { + const vsxEnvironment = ctx.container.get(VSXEnvironment); + const clientProvider = ctx.container.get(OVSXClientProvider); + const client = await clientProvider(); + const apiVersion = await vsxEnvironment.getVscodeApiVersion(); + const apiFilter = new OVSXApiFilterImpl(client, apiVersion); + return apiFilter; + }); }); diff --git a/packages/vsx-registry/src/node/vsx-cli-deployer-participant.ts b/packages/vsx-registry/src/node/vsx-cli-deployer-participant.ts new file mode 100644 index 0000000000000..b7b34c7c1ce08 --- /dev/null +++ b/packages/vsx-registry/src/node/vsx-cli-deployer-participant.ts @@ -0,0 +1,46 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { PluginDeployerParticipant, PluginDeployerStartContext } from '@theia/plugin-ext'; +import { VsxCli } from './vsx-cli'; +import { VSXExtensionUri } from '../common'; +import * as fs from 'fs'; +import { FileUri } from '@theia/core/lib/node'; +import * as path from 'path'; + +@injectable() +export class VsxCliDeployerParticipant implements PluginDeployerParticipant { + + @inject(VsxCli) + protected readonly vsxCli: VsxCli; + + async onWillStart(context: PluginDeployerStartContext): Promise { + const pluginUris = await Promise.all(this.vsxCli.pluginsToInstall.map(async id => { + try { + const resolvedPath = path.resolve(id); + const stat = await fs.promises.stat(resolvedPath); + if (stat.isFile()) { + return FileUri.create(resolvedPath).withScheme('local-file').toString(); + } + } catch (e) { + // expected if file does not exist + } + return VSXExtensionUri.fromVersionedId(id).toString(); + })); + context.userEntries.push(...pluginUris); + } +} diff --git a/packages/vsx-registry/src/node/vsx-cli.ts b/packages/vsx-registry/src/node/vsx-cli.ts index c0ce5a9f06c61..2b3efab2a4cc8 100644 --- a/packages/vsx-registry/src/node/vsx-cli.ts +++ b/packages/vsx-registry/src/node/vsx-cli.ts @@ -17,16 +17,24 @@ import { CliContribution } from '@theia/core/lib/node'; import { injectable } from '@theia/core/shared/inversify'; import { Argv } from '@theia/core/shared/yargs'; -import { OVSXRouterConfig } from '@theia/ovsx-client'; +import { OVSX_RATE_LIMIT, OVSXRouterConfig } from '@theia/ovsx-client'; import * as fs from 'fs'; @injectable() export class VsxCli implements CliContribution { ovsxRouterConfig: OVSXRouterConfig | undefined; + ovsxRateLimit: number; + pluginsToInstall: string[] = []; configure(conf: Argv<{}>): void { conf.option('ovsx-router-config', { description: 'JSON configuration file for the OVSX router client', type: 'string' }); + conf.option('ovsx-rate-limit', { description: 'Limits the number of requests to OVSX per second', type: 'number', default: OVSX_RATE_LIMIT }); + conf.option('install-plugin', { + alias: 'install-extension', + nargs: 1, + desc: 'Installs or updates a plugin. Argument is a path to the *.vsix file or a plugin id of the form "publisher.name[@version]"' + }); } async setArguments(args: Record): Promise { @@ -34,5 +42,14 @@ export class VsxCli implements CliContribution { if (typeof ovsxRouterConfig === 'string') { this.ovsxRouterConfig = JSON.parse(await fs.promises.readFile(ovsxRouterConfig, 'utf8')); } + let pluginsToInstall = args.installPlugin; + if (typeof pluginsToInstall === 'string') { + pluginsToInstall = [pluginsToInstall]; + } + if (Array.isArray(pluginsToInstall)) { + this.pluginsToInstall = pluginsToInstall; + } + const ovsxRateLimit = args.ovsxRateLimit; + this.ovsxRateLimit = typeof ovsxRateLimit === 'number' ? ovsxRateLimit : OVSX_RATE_LIMIT; } } diff --git a/packages/vsx-registry/src/node/vsx-environment-impl.ts b/packages/vsx-registry/src/node/vsx-environment-impl.ts index ff094b09c41f7..8515650c5d119 100644 --- a/packages/vsx-registry/src/node/vsx-environment-impl.ts +++ b/packages/vsx-registry/src/node/vsx-environment-impl.ts @@ -32,6 +32,10 @@ export class VSXEnvironmentImpl implements VSXEnvironment { @inject(VsxCli) protected vsxCli: VsxCli; + async getRateLimit(): Promise { + return this.vsxCli.ovsxRateLimit; + } + async getRegistryUri(): Promise { return this._registryUri.toString(true); } diff --git a/packages/vsx-registry/src/node/vsx-extension-resolver.ts b/packages/vsx-registry/src/node/vsx-extension-resolver.ts index cfe6ab3d09369..5f21d3cb53df0 100644 --- a/packages/vsx-registry/src/node/vsx-extension-resolver.ts +++ b/packages/vsx-registry/src/node/vsx-extension-resolver.ts @@ -20,9 +20,10 @@ import * as fs from '@theia/core/shared/fs-extra'; import { injectable, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { PluginDeployerHandler, PluginDeployerResolver, PluginDeployerResolverContext, PluginDeployOptions, PluginIdentifiers } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { FileUri } from '@theia/core/lib/node'; import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri'; import { OVSXClientProvider } from '../common/ovsx-client-provider'; -import { OVSXApiFilter, VSXExtensionRaw } from '@theia/ovsx-client'; +import { OVSXApiFilterProvider, VSXExtensionRaw, VSXTargetPlatform } from '@theia/ovsx-client'; import { RequestService } from '@theia/core/shared/@theia/request'; import { PluginVSCodeEnvironment } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-environment'; import { PluginUninstallationManager } from '@theia/plugin-ext/lib/main/node/plugin-uninstallation-manager'; @@ -35,27 +36,38 @@ export class VSXExtensionResolver implements PluginDeployerResolver { @inject(RequestService) protected requestService: RequestService; @inject(PluginVSCodeEnvironment) protected readonly environment: PluginVSCodeEnvironment; @inject(PluginUninstallationManager) protected readonly uninstallationManager: PluginUninstallationManager; - @inject(OVSXApiFilter) protected vsxApiFilter: OVSXApiFilter; + @inject(OVSXApiFilterProvider) protected vsxApiFilter: OVSXApiFilterProvider; accept(pluginId: string): boolean { return !!VSCodeExtensionUri.toId(new URI(pluginId)); } + static readonly TEMP_DIR_PREFIX = 'vscode-download'; + static readonly TARGET_PLATFORM = `${process.platform}-${process.arch}` as VSXTargetPlatform; + async resolve(context: PluginDeployerResolverContext, options?: PluginDeployOptions): Promise { const id = VSCodeExtensionUri.toId(new URI(context.getOriginId())); if (!id) { return; } let extension: VSXExtensionRaw | undefined; - const client = await this.clientProvider(); - if (options) { - console.log(`[${id}]: trying to resolve version ${options.version}...`); - const { extensions } = await client.query({ extensionId: id, extensionVersion: options.version, includeAllVersions: true }); - extension = extensions[0]; + const filter = await this.vsxApiFilter(); + const version = options?.version || id.version; + if (version) { + console.log(`[${id.id}]: trying to resolve version ${version}...`); + extension = await filter.findLatestCompatibleExtension({ + extensionId: id.id, + extensionVersion: version, + includeAllVersions: true, + targetPlatform: VSXExtensionResolver.TARGET_PLATFORM + }); } else { - console.log(`[${id}]: trying to resolve latest version...`); - const { extensions } = await client.query({ extensionId: id, includeAllVersions: true }); - extension = this.vsxApiFilter.getLatestCompatibleExtension(extensions); + console.log(`[${id.id}]: trying to resolve latest version...`); + extension = await filter.findLatestCompatibleExtension({ + extensionId: id.id, + includeAllVersions: true, + targetPlatform: VSXExtensionResolver.TARGET_PLATFORM + }); } if (!extension) { return; @@ -63,27 +75,35 @@ export class VSXExtensionResolver implements PluginDeployerResolver { if (extension.error) { throw new Error(extension.error); } - const resolvedId = id + '-' + extension.version; + const resolvedId = id.id + '-' + extension.version; const downloadUrl = extension.files.download; - console.log(`[${id}]: resolved to '${resolvedId}'`); + console.log(`[${id.id}]: resolved to '${resolvedId}'`); if (!options?.ignoreOtherVersions) { - const existingVersion = this.hasSameOrNewerVersion(id, extension); + const existingVersion = this.hasSameOrNewerVersion(id.id, extension); if (existingVersion) { - console.log(`[${id}]: is already installed with the same or newer version '${existingVersion}'`); + console.log(`[${id.id}]: is already installed with the same or newer version '${existingVersion}'`); return; } } - const downloadPath = (await this.environment.getExtensionsDirUri()).path.fsPath(); - await fs.ensureDir(downloadPath); - const extensionPath = path.resolve(downloadPath, path.basename(downloadUrl)); - console.log(`[${resolvedId}]: trying to download from "${downloadUrl}"...`, 'to path', downloadPath); - if (!await this.download(downloadUrl, extensionPath)) { + const downloadDir = await this.getTempDir(); + await fs.ensureDir(downloadDir); + const downloadedExtensionPath = path.resolve(downloadDir, path.basename(downloadUrl)); + console.log(`[${resolvedId}]: trying to download from "${downloadUrl}"...`, 'to path', downloadDir); + if (!await this.download(downloadUrl, downloadedExtensionPath)) { console.log(`[${resolvedId}]: not found`); return; } - console.log(`[${resolvedId}]: downloaded to ${extensionPath}"`); - context.addPlugin(resolvedId, extensionPath); + console.log(`[${resolvedId}]: downloaded to ${downloadedExtensionPath}"`); + context.addPlugin(resolvedId, downloadedExtensionPath); + } + + protected async getTempDir(): Promise { + const tempDir = FileUri.fsPath(await this.environment.getTempDirUri(VSXExtensionResolver.TEMP_DIR_PREFIX)); + if (!await fs.pathExists(tempDir)) { + await fs.mkdirs(tempDir); + } + return tempDir; } protected hasSameOrNewerVersion(id: string, extension: VSXExtensionRaw): string | undefined { diff --git a/packages/vsx-registry/src/node/vsx-registry-backend-module.ts b/packages/vsx-registry/src/node/vsx-registry-backend-module.ts index b060f4bc91a84..2f590c01b2578 100644 --- a/packages/vsx-registry/src/node/vsx-registry-backend-module.ts +++ b/packages/vsx-registry/src/node/vsx-registry-backend-module.ts @@ -17,11 +17,12 @@ import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core'; import { CliContribution } from '@theia/core/lib/node'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { PluginDeployerResolver } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { PluginDeployerParticipant, PluginDeployerResolver } from '@theia/plugin-ext/lib/common/plugin-protocol'; import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment'; import { VsxCli } from './vsx-cli'; import { VSXEnvironmentImpl } from './vsx-environment-impl'; import { VSXExtensionResolver } from './vsx-extension-resolver'; +import { VsxCliDeployerParticipant } from './vsx-cli-deployer-participant'; export default new ContainerModule(bind => { bind(VSXEnvironment).to(VSXEnvironmentImpl).inSingletonScope(); @@ -32,4 +33,6 @@ export default new ContainerModule(bind => { .inSingletonScope(); bind(VSXExtensionResolver).toSelf().inSingletonScope(); bind(PluginDeployerResolver).toService(VSXExtensionResolver); + bind(VsxCliDeployerParticipant).toSelf().inSingletonScope(); + bind(PluginDeployerParticipant).toService(VsxCliDeployerParticipant); }); diff --git a/packages/vsx-registry/src/node/vsx-remote-cli.ts b/packages/vsx-registry/src/node/vsx-remote-cli.ts new file mode 100644 index 0000000000000..dc75d9c2b16d7 --- /dev/null +++ b/packages/vsx-registry/src/node/vsx-remote-cli.ts @@ -0,0 +1,39 @@ +// ***************************************************************************** +// 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 { RemoteCliContext, RemoteCliContribution } from '@theia/core/lib/node/remote/remote-cli-contribution'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { PluginDeployerHandler, PluginType } from '@theia/plugin-ext'; + +@injectable() +export class VsxRemoteCli implements RemoteCliContribution { + + @inject(PluginDeployerHandler) + protected readonly pluginDeployerHandler: PluginDeployerHandler; + + async enhanceArgs(context: RemoteCliContext): Promise { + const deployedPlugins = await this.pluginDeployerHandler.getDeployedPlugins(); + // Plugin IDs can be duplicated between frontend and backend plugins, so we create a set first + const installPluginArgs = Array.from( + new Set( + deployedPlugins + .filter(plugin => plugin.type === PluginType.User) + .map(p => `--install-plugin=${p.metadata.model.id}`) + ) + ); + return installPluginArgs; + } +} diff --git a/packages/vsx-registry/tsconfig.json b/packages/vsx-registry/tsconfig.json index ba51f2ea3a5ee..c2d9957cb1436 100644 --- a/packages/vsx-registry/tsconfig.json +++ b/packages/vsx-registry/tsconfig.json @@ -18,6 +18,9 @@ { "path": "../filesystem" }, + { + "path": "../navigator" + }, { "path": "../plugin-ext" }, diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 6eee65185071b..4480fb4d92c12 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -1,12 +1,13 @@ { "name": "@theia/workspace", - "version": "1.44.0", + "version": "1.54.0", "description": "Theia - Workspace Extension", "dependencies": { - "@theia/core": "1.44.0", - "@theia/filesystem": "1.44.0", - "@theia/variable-resolver": "1.44.0", + "@theia/core": "1.54.0", + "@theia/filesystem": "1.54.0", + "@theia/variable-resolver": "1.54.0", "jsonc-parser": "^2.2.0", + "tslib": "^2.6.2", "valid-filename": "^2.0.1" }, "publishConfig": { @@ -16,6 +17,9 @@ { "frontend": "lib/browser/workspace-frontend-module", "backend": "lib/node/workspace-backend-module" + }, + { + "frontendOnly": "lib/browser-only/workspace-frontend-only-module" } ], "keywords": [ @@ -43,7 +47,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.44.0" + "@theia/ext-scripts": "1.54.0" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/workspace/src/browser-only/browser-only-workspace-server.ts b/packages/workspace/src/browser-only/browser-only-workspace-server.ts new file mode 100644 index 0000000000000..2c94b3b997791 --- /dev/null +++ b/packages/workspace/src/browser-only/browser-only-workspace-server.ts @@ -0,0 +1,69 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { WorkspaceServer } from '../common/workspace-protocol'; +import { ILogger, isStringArray } from '@theia/core'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; + +export const RECENT_WORKSPACES_LOCAL_STORAGE_KEY = 'workspaces'; + +@injectable() +export class BrowserOnlyWorkspaceServer implements WorkspaceServer { + + @inject(ILogger) + protected logger: ILogger; + + @inject(FileService) + protected readonly fileService: FileService; + + async getRecentWorkspaces(): Promise { + const storedWorkspaces = localStorage.getItem(RECENT_WORKSPACES_LOCAL_STORAGE_KEY); + if (!storedWorkspaces) { + return []; + } + try { + const parsedWorkspaces = JSON.parse(storedWorkspaces); + if (isStringArray(parsedWorkspaces)) { + return parsedWorkspaces; + } + } catch (e) { + this.logger.error(e); + return []; + } + return []; + } + + async getMostRecentlyUsedWorkspace(): Promise { + const workspaces = await this.getRecentWorkspaces(); + return workspaces[0]; + } + + async setMostRecentlyUsedWorkspace(uri: string): Promise { + const workspaces = await this.getRecentWorkspaces(); + if (workspaces.includes(uri)) { + workspaces.splice(workspaces.indexOf(uri), 1); + } + localStorage.setItem(RECENT_WORKSPACES_LOCAL_STORAGE_KEY, JSON.stringify([uri, ...workspaces])); + } + + async removeRecentWorkspace(uri: string): Promise { + const workspaces = await this.getRecentWorkspaces(); + if (workspaces.includes(uri)) { + workspaces.splice(workspaces.indexOf(uri), 1); + } + localStorage.setItem(RECENT_WORKSPACES_LOCAL_STORAGE_KEY, JSON.stringify(workspaces)); + } +} diff --git a/packages/workspace/src/browser-only/workspace-frontend-only-module.ts b/packages/workspace/src/browser-only/workspace-frontend-only-module.ts new file mode 100644 index 0000000000000..2953f66fdfd2d --- /dev/null +++ b/packages/workspace/src/browser-only/workspace-frontend-only-module.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// 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 { BrowserOnlyWorkspaceServer } from './browser-only-workspace-server'; +import { WorkspaceServer } from '../common'; + +export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { + bind(BrowserOnlyWorkspaceServer).toSelf().inSingletonScope(); + if (isBound(WorkspaceServer)) { + rebind(WorkspaceServer).toService(BrowserOnlyWorkspaceServer); + } else { + bind(WorkspaceServer).toService(BrowserOnlyWorkspaceServer); + } +}); diff --git a/packages/workspace/src/browser/quick-open-workspace.ts b/packages/workspace/src/browser/quick-open-workspace.ts index 473c27400d17a..91890ec2bf89f 100644 --- a/packages/workspace/src/browser/quick-open-workspace.ts +++ b/packages/workspace/src/browser/quick-open-workspace.ts @@ -18,10 +18,7 @@ import { injectable, inject, optional } from '@theia/core/shared/inversify'; import { QuickPickItem, LabelProvider, QuickInputService, QuickInputButton, QuickPickSeparator } from '@theia/core/lib/browser'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { WorkspaceService } from './workspace-service'; -import { WorkspacePreferences } from './workspace-preferences'; import URI from '@theia/core/lib/common/uri'; -import { FileService } from '@theia/filesystem/lib/browser/file-service'; -import { FileStat } from '@theia/filesystem/lib/common/files'; import { nls, Path } from '@theia/core/lib/common'; import { UntitledWorkspaceService } from '../common/untitled-workspace-service'; @@ -31,14 +28,11 @@ interface RecentlyOpenedPick extends QuickPickItem { @injectable() export class QuickOpenWorkspace { - protected items: Array; protected opened: boolean; @inject(QuickInputService) @optional() protected readonly quickInputService: QuickInputService; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; - @inject(FileService) protected readonly fileService: FileService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; - @inject(WorkspacePreferences) protected preferences: WorkspacePreferences; @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer; @inject(UntitledWorkspaceService) protected untitledWorkspaceService: UntitledWorkspaceService; @@ -48,45 +42,34 @@ export class QuickOpenWorkspace { }; async open(workspaces: string[]): Promise { - this.items = []; - const [homeDirUri] = await Promise.all([ - this.envServer.getHomeDirUri(), - this.workspaceService.getUntitledWorkspace() - ]); - const home = new URI(homeDirUri).path.toString(); - await this.preferences.ready; - this.items.push({ + const homeDirUri = await this.envServer.getHomeDirUri(); + const home = new URI(homeDirUri).path.fsPath(); + const items: (RecentlyOpenedPick | QuickPickSeparator)[] = [{ type: 'separator', label: nls.localizeByDefault('folders & workspaces') - }); + }]; + for (const workspace of workspaces) { const uri = new URI(workspace); - let stat: FileStat | undefined; - try { - stat = await this.fileService.resolve(uri); - } catch { } - if (this.untitledWorkspaceService.isUntitledWorkspace(uri) || !stat) { - continue; // skip the temporary workspace files or an undefined stat. + const label = uri.path.base; + if (!label || this.untitledWorkspaceService.isUntitledWorkspace(uri)) { + continue; // skip temporary workspace files & empty workspace names } - const icon = this.labelProvider.getIcon(stat); - const iconClasses = icon === '' ? undefined : [icon + ' file-icon']; - - this.items.push({ - label: uri.path.base, - description: Path.tildify(uri.path.toString(), home), - iconClasses, + items.push({ + label: label, + description: Path.tildify(uri.path.fsPath(), home), buttons: [this.removeRecentWorkspaceButton], resource: uri, execute: () => { const current = this.workspaceService.workspace; - const uriToOpen = new URI(workspace); if ((current && current.resource.toString() !== workspace) || !current) { - this.workspaceService.open(uriToOpen); + this.workspaceService.open(uri); } - }, + } }); } - this.quickInputService?.showQuickPick(this.items, { + + this.quickInputService?.showQuickPick(items, { placeholder: nls.localize( 'theia/workspace/openRecentPlaceholder', 'Type the name of the workspace you want to open'), @@ -101,7 +84,6 @@ export class QuickOpenWorkspace { } select(): void { - this.items = []; this.opened = this.workspaceService.opened; this.workspaceService.recentWorkspaces().then(workspaceRoots => { if (workspaceRoots) { diff --git a/packages/workspace/src/browser/workspace-commands.ts b/packages/workspace/src/browser/workspace-commands.ts index 2e7625f434485..f000ffacf755f 100644 --- a/packages/workspace/src/browser/workspace-commands.ts +++ b/packages/workspace/src/browser/workspace-commands.ts @@ -98,6 +98,7 @@ export namespace WorkspaceCommands { category: FILE_CATEGORY, label: 'New Folder...' }); + /** @deprecated Use the `OpenWithService` instead */ export const FILE_OPEN_WITH = (opener: OpenHandler): Command => ({ id: `file.openWith.${opener.id}` }); @@ -224,7 +225,6 @@ export class WorkspaceCommandContribution implements CommandContribution { } registerCommands(registry: CommandRegistry): void { - this.registerOpenWith(registry); registry.registerCommand(WorkspaceCommands.NEW_FILE, this.newWorkspaceRootUriAwareCommandHandler({ execute: uri => this.getDirectory(uri).then(parent => { if (parent) { @@ -356,24 +356,6 @@ export class WorkspaceCommandContribution implements CommandContribution { })); } - openers: OpenHandler[]; - protected async registerOpenWith(registry: CommandRegistry): Promise { - if (this.openerService.onDidChangeOpeners) { - this.openerService.onDidChangeOpeners(async e => { - this.openers = await this.openerService.getOpeners(); - }); - } - this.openers = await this.openerService.getOpeners(); - for (const opener of this.openers) { - const openWithCommand = WorkspaceCommands.FILE_OPEN_WITH(opener); - registry.registerCommand(openWithCommand, this.newUriAwareCommandHandler({ - execute: uri => opener.open(uri), - isEnabled: uri => opener.canHandle(uri) > 0, - isVisible: uri => opener.canHandle(uri) > 0 && this.areMultipleOpenHandlersPresent(this.openers, uri) - })); - } - } - protected newUriAwareCommandHandler(handler: UriCommandHandler): UriAwareCommandHandler { return UriAwareCommandHandler.MonoSelect(this.selectionService, handler); } @@ -537,19 +519,6 @@ export class WorkspaceCommandContribution implements CommandContribution { return registry.executeCommand(saveCommand.id); } } - - protected areMultipleOpenHandlersPresent(openers: OpenHandler[], uri: URI): boolean { - let count = 0; - for (const opener of openers) { - if (opener.canHandle(uri) > 0) { - count++; - } - if (count > 1) { - return true; - } - } - return false; - } } export class WorkspaceRootUriAwareCommandHandler extends UriAwareCommandHandler { @@ -575,8 +544,8 @@ export class WorkspaceRootUriAwareCommandHandler extends UriAwareCommandHandler< // eslint-disable-next-line @typescript-eslint/no-explicit-any protected override getUri(...args: any[]): URI | undefined { const uri = super.getUri(...args); - // Return the `uri` immediately if the resource exists in any of the workspace roots and is of `file` scheme. - if (uri && uri.scheme === 'file' && this.workspaceService.getWorkspaceRootUri(uri)) { + // Return the `uri` immediately if the resource exists in any of the workspace roots. + if (uri && this.workspaceService.getWorkspaceRootUri(uri)) { return uri; } // Return the first root if available. diff --git a/packages/workspace/src/browser/workspace-frontend-contribution.ts b/packages/workspace/src/browser/workspace-frontend-contribution.ts index 6a5fb2060cf95..21ae2698d297d 100644 --- a/packages/workspace/src/browser/workspace-frontend-contribution.ts +++ b/packages/workspace/src/browser/workspace-frontend-contribution.ts @@ -37,7 +37,7 @@ import { nls } from '@theia/core/lib/common/nls'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { FileStat } from '@theia/filesystem/lib/common/files'; import { UntitledWorkspaceExitDialog } from './untitled-workspace-exit-dialog'; -import { FilesystemSaveResourceService } from '@theia/filesystem/lib/browser/filesystem-save-resource-service'; +import { FilesystemSaveableService } from '@theia/filesystem/lib/browser/filesystem-saveable-service'; import { StopReason } from '@theia/core/lib/common/frontend-application-state'; export enum WorkspaceStates { @@ -72,7 +72,7 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; @inject(EncodingRegistry) protected readonly encodingRegistry: EncodingRegistry; @inject(PreferenceConfigurations) protected readonly preferenceConfigurations: PreferenceConfigurations; - @inject(FilesystemSaveResourceService) protected readonly saveService: FilesystemSaveResourceService; + @inject(FilesystemSaveableService) protected readonly saveService: FilesystemSaveableService; @inject(WorkspaceFileService) protected readonly workspaceFileService: WorkspaceFileService; configure(): void { @@ -456,7 +456,7 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi } async saveAs(widget: Widget & SaveableSource & Navigatable): Promise { - return this.saveService.saveAs(widget); + await this.saveService.saveAs(widget); } protected updateWorkspaceStateKey(): WorkspaceState { diff --git a/packages/workspace/src/browser/workspace-service.ts b/packages/workspace/src/browser/workspace-service.ts index 81f1ff8c0468e..44eeb15e58043 100644 --- a/packages/workspace/src/browser/workspace-service.ts +++ b/packages/workspace/src/browser/workspace-service.ts @@ -404,8 +404,12 @@ export class WorkspaceService implements FrontendApplicationContribution { } async spliceRoots(start: number, deleteCount?: number, ...rootsToAdd: URI[]): Promise { - if (!this._workspace) { - throw new Error('There is no active workspace'); + if (!this._workspace || this._workspace.isDirectory) { + const untitledWorkspace = await this.getUntitledWorkspace(); + await this.save(untitledWorkspace); + if (!this._workspace) { + throw new Error('Could not create new untitled workspace'); + } } const dedup = new Set(); const roots = this._roots.map(root => (dedup.add(root.resource.toString()), root.resource.toString())); @@ -421,10 +425,7 @@ export class WorkspaceService implements FrontendApplicationContribution { if (!toRemove.length && !toAdd.length) { return []; } - if (this._workspace.isDirectory) { - const untitledWorkspace = await this.getUntitledWorkspace(); - await this.save(untitledWorkspace); - } + const currentData = await this.getWorkspaceDataFromFile(); const newData = WorkspaceData.buildWorkspaceData(roots, currentData); await this.writeWorkspaceFile(this._workspace, newData); @@ -659,7 +660,7 @@ export class WorkspaceService implements FrontendApplicationContribution { const rootUris: URI[] = []; for (const root of this.tryGetRoots()) { const rootUri = root.resource; - if (rootUri && rootUri.isEqualOrParent(uri)) { + if (rootUri && rootUri.scheme === uri.scheme && rootUri.isEqualOrParent(uri)) { rootUris.push(rootUri); } } diff --git a/packages/workspace/src/browser/workspace-trust-preferences.ts b/packages/workspace/src/browser/workspace-trust-preferences.ts index 228c10ca55768..7657545e37760 100644 --- a/packages/workspace/src/browser/workspace-trust-preferences.ts +++ b/packages/workspace/src/browser/workspace-trust-preferences.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { - createPreferenceProxy, PreferenceContribution, PreferenceProxy, PreferenceSchema, PreferenceService + createPreferenceProxy, PreferenceContribution, PreferenceProxy, PreferenceSchema, PreferenceScope, PreferenceService } from '@theia/core/lib/browser/preferences'; import { nls } from '@theia/core/lib/common/nls'; import { interfaces } from '@theia/core/shared/inversify'; @@ -32,6 +32,7 @@ export enum WorkspaceTrustPrompt { export const workspaceTrustPreferenceSchema: PreferenceSchema = { type: 'object', + scope: PreferenceScope.User, properties: { [WORKSPACE_TRUST_ENABLED]: { description: nls.localize('theia/workspace/trustEnabled', 'Controls whether or not workspace trust is enabled. If disabled, all workspaces are trusted.'), diff --git a/packages/workspace/src/browser/workspace-trust-service.ts b/packages/workspace/src/browser/workspace-trust-service.ts index 6572da09024ee..6aaa7a7e7b6ad 100644 --- a/packages/workspace/src/browser/workspace-trust-service.ts +++ b/packages/workspace/src/browser/workspace-trust-service.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { ConfirmDialog, Dialog, PreferenceChange, StorageService } from '@theia/core/lib/browser'; -import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; +import { PreferenceScope, PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; import { MessageService } from '@theia/core/lib/common/message-service'; import { nls } from '@theia/core/lib/common/nls'; import { Deferred } from '@theia/core/lib/common/promise-util'; @@ -26,6 +26,7 @@ import { } from './workspace-trust-preferences'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { WorkspaceService } from './workspace-service'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; const STORAGE_TRUSTED = 'trusted'; @@ -49,6 +50,9 @@ export class WorkspaceTrustService { @inject(WindowService) protected readonly windowService: WindowService; + @inject(ContextKeyService) + protected readonly contextKeyService: ContextKeyService; + protected workspaceTrust = new Deferred(); @postConstruct() @@ -71,6 +75,7 @@ export class WorkspaceTrustService { const trust = givenTrust ?? await this.calculateWorkspaceTrust(); if (trust !== undefined) { await this.storeWorkspaceTrust(trust); + this.contextKeyService.setContext('isWorkspaceTrusted', trust); this.workspaceTrust.resolve(trust); } } @@ -110,17 +115,19 @@ export class WorkspaceTrustService { } protected async handlePreferenceChange(change: PreferenceChange): Promise { - if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT && change.newValue !== WorkspaceTrustPrompt.ONCE) { - this.storage.setData(STORAGE_TRUSTED, undefined); - } + if (change.scope === PreferenceScope.User) { + if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT && change.newValue !== WorkspaceTrustPrompt.ONCE) { + this.storage.setData(STORAGE_TRUSTED, undefined); + } - if (change.preferenceName === WORKSPACE_TRUST_ENABLED && this.isWorkspaceTrustResolved() && await this.confirmRestart()) { - this.windowService.setSafeToShutDown(); - this.windowService.reload(); - } + if (change.preferenceName === WORKSPACE_TRUST_ENABLED && this.isWorkspaceTrustResolved() && await this.confirmRestart()) { + this.windowService.setSafeToShutDown(); + this.windowService.reload(); + } - if (change.preferenceName === WORKSPACE_TRUST_ENABLED || change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW) { - this.resolveWorkspaceTrust(); + if (change.preferenceName === WORKSPACE_TRUST_ENABLED || change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW) { + this.resolveWorkspaceTrust(); + } } } diff --git a/packages/workspace/src/browser/workspace-user-working-directory-provider.ts b/packages/workspace/src/browser/workspace-user-working-directory-provider.ts index 26d4121f7add8..af8f69d2a2cb5 100644 --- a/packages/workspace/src/browser/workspace-user-working-directory-provider.ts +++ b/packages/workspace/src/browser/workspace-user-working-directory-provider.ts @@ -28,6 +28,7 @@ export class WorkspaceUserWorkingDirectoryProvider extends UserWorkingDirectoryP override async getUserWorkingDir(): Promise { return await this.getFromSelection() + ?? await this.getFromLastOpenResource() ?? await this.getFromWorkspace() ?? this.getFromUserHome(); } diff --git a/packages/workspace/src/node/default-workspace-server.ts b/packages/workspace/src/node/default-workspace-server.ts index 10a7490fc41a5..251d8dc6f54de 100644 --- a/packages/workspace/src/node/default-workspace-server.ts +++ b/packages/workspace/src/node/default-workspace-server.ts @@ -161,7 +161,12 @@ export class DefaultWorkspaceServer implements WorkspaceServer, BackendApplicati } protected async workspaceStillExist(workspaceRootUri: string): Promise { - return fs.pathExists(FileUri.fsPath(workspaceRootUri)); + const uri = new URI(workspaceRootUri); + // Non file system workspaces cannot be checked for existence + if (uri.scheme !== 'file') { + return false; + } + return fs.pathExists(uri.path.fsPath()); } protected async getWorkspaceURIFromCli(): Promise { diff --git a/sample-plugins/sample-namespace/plugin-a/package.json b/sample-plugins/sample-namespace/plugin-a/package.json index 550d6df713ff4..8b1af99d4c01f 100644 --- a/sample-plugins/sample-namespace/plugin-a/package.json +++ b/sample-plugins/sample-namespace/plugin-a/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "plugin-a", - "version": "1.44.0", + "version": "1.54.0", "main": "extension.js", "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0", "repository": { @@ -9,7 +9,7 @@ "url": "https://github.com/eclipse-theia/theia.git" }, "engines": { - "vscode": "^1.50.0" + "vscode": "^1.51.0" }, "activationEvents": [ "onCommand:plugin-a.hello" diff --git a/sample-plugins/sample-namespace/plugin-b/package.json b/sample-plugins/sample-namespace/plugin-b/package.json index 28db7507856b8..6f195fb4b3569 100644 --- a/sample-plugins/sample-namespace/plugin-b/package.json +++ b/sample-plugins/sample-namespace/plugin-b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "plugin-b", - "version": "1.44.0", + "version": "1.54.0", "main": "extension.js", "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0", "repository": { @@ -9,7 +9,7 @@ "url": "https://github.com/eclipse-theia/theia.git" }, "engines": { - "vscode": "^1.50.0" + "vscode": "^1.51.0" }, "activationEvents": [ "onCommand:plugin-b.hello" diff --git a/sample-plugins/sample-namespace/plugin-gotd/.gitignore b/sample-plugins/sample-namespace/plugin-gotd/.gitignore new file mode 100644 index 0000000000000..aa1ec1ea06181 --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-gotd/.gitignore @@ -0,0 +1 @@ +*.tgz diff --git a/sample-plugins/sample-namespace/plugin-gotd/LICENSE b/sample-plugins/sample-namespace/plugin-gotd/LICENSE new file mode 100644 index 0000000000000..e48e0963459bf --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-gotd/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"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: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/sample-plugins/sample-namespace/plugin-gotd/README.md b/sample-plugins/sample-namespace/plugin-gotd/README.md new file mode 100644 index 0000000000000..f16aeb4a4ca6a --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-gotd/README.md @@ -0,0 +1,44 @@ +
    + +
    + +theia-ext-logo + +

    ECLIPSE THEIA - EXAMPLE HEADLESS PLUGIN USING THE API PROVIDER SAMPLE

    + +
    + +
    + +## Description + +An example demonstrating three Theia concepts: + +- "headless plugins", being plugins loaded in a single plugin host Node process outside of the context of any frontend connection +- client of a custom "Greeting of the Day" API provided by the `@theia/api-provider-sample` extension +- "backend plugins", being plugins loaded in the backend plugin host process for a frontend connection + +Thus this plug-in demonstrates the capability of a VS Code-compatible plugin to provide two distinct backend entry-points for the two different backend contexts. +As declared in the `package.json` manifest: +- in the headless plugin host, the entry-point script is `headless.js` via the Theia-specific the `"theiaPlugin"` object +- in the backend plugin host for a frontend connection, the entry-point script is `backend.js` via the VS Code standard `"main"` property + +The plugin is for reference and test purposes only and is not published on `npm` (`private: true`). + +### Greeting of the Day + +The sample uses the custom `gotd` API to log a greeting upon activation. + +## 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/sample-plugins/sample-namespace/plugin-gotd/extension.js b/sample-plugins/sample-namespace/plugin-gotd/extension.js new file mode 100644 index 0000000000000..c902426005a8a --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-gotd/extension.js @@ -0,0 +1,41 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +const vscode = require('vscode'); + +function extensionKind(kind) { + switch (kind) { + case vscode.ExtensionKind.UI: + return 'UI'; + case vscode.ExtensionKind.Workspace: + return 'Workspace'; + default: + return 'unknown'; + } +} + +async function activate () { + console.log('[GOTD-BE]', `Running version ${vscode.version} of the VS Code Extension API.`); + console.log('[GOTD-BE]', `It looks like your shell is ${vscode.env.shell}.`); + const myself = vscode.extensions.getExtension('.plugin-gotd'); + if (myself) { + console.log('[GOTD-BE]', `And I am a(n) ${extensionKind(myself.extensionKind)} plugin installed at ${myself.extensionPath}.`); + } +} + +module.exports = { + activate +}; diff --git a/sample-plugins/sample-namespace/plugin-gotd/headless.js b/sample-plugins/sample-namespace/plugin-gotd/headless.js new file mode 100644 index 0000000000000..40a6435927e93 --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-gotd/headless.js @@ -0,0 +1,88 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +const gotd = require('@theia/api-provider-sample'); + +const GreetingKind = gotd.greeting.GreetingKind; + +const toDispose = []; + +function greetingKindsToString(greetingKinds) { + return greetingKinds.map(kind => { + switch (kind) { + case GreetingKind.DIRECT: + return 'DIRECT'; + case GreetingKind.QUIRKY: + return 'QUIRKY'; + case GreetingKind.SNARKY: + return 'SNARKY'; + default: + return ''; + } + }).join(', '); +} + +async function greet(greeter) { + const message = await greeter.getMessage(); + console.log('[GOTD]', message); +} + +let busy = false; +const pending = []; +function later(_fn) { + const task = (fn) => () => { + fn(); + const next = pending.shift(); + if (next) { + setTimeout(task(next), 1000); + } else { + busy = false; + } + } + + if (busy) { + pending.push(_fn); + } else { + busy = true; + setTimeout(task(_fn), 1000); + } +} + +async function activate () { + const greeter = await gotd.greeting.createGreeter(); + toDispose.push(greeter.onGreetingKindsChanged( + kinds => { + console.log('[GOTD]', + `Now supporting these kinds of greeting: ${greetingKindsToString(kinds)}.`); + if (kinds.length > 0) { + greet(greeter); + } + })); + + greet(greeter); + + later(() => greeter.setGreetingKind(GreetingKind.DIRECT, false)); + later(() => greeter.setGreetingKind(GreetingKind.QUIRKY)); + later(() => greeter.setGreetingKind(GreetingKind.SNARKY)); +} + +module.exports = { + activate, + deactivate: function () { + console.log('[GOTD]', 'Cleaning up.'); + toDispose.forEach(d => d.dispose()); + } +}; diff --git a/sample-plugins/sample-namespace/plugin-gotd/package.json b/sample-plugins/sample-namespace/plugin-gotd/package.json new file mode 100644 index 0000000000000..0eae6cb2417db --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-gotd/package.json @@ -0,0 +1,33 @@ +{ + "private": true, + "name": "plugin-gotd", + "version": "1.54.0", + "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" + }, + "engines": { + "vscode": "^1.84.0" + }, + "main": "extension", + "activationEvents": [ + "*" + ], + "devDependencies": { + "@theia/api-provider-sample": "1.54.0" + }, + "scripts": { + "prepare": "yarn -s package", + "package": "yarn pack" + }, + "theiaPlugin": { + "headless": "headless" + }, + "headless": { + "activationEvents": [ + "*" + ], + "contributes": {} + } +} diff --git a/scripts/translation-update.js b/scripts/translation-update.js index caec3cbaca59b..c90cce7d9bcfc 100644 --- a/scripts/translation-update.js +++ b/scripts/translation-update.js @@ -23,7 +23,8 @@ function performNlsExtract() { '-e', 'vscode', '-f', './packages/**/browser/**/*.{ts,tsx}' ], { - shell: true + shell: true, + stdio: 'inherit' }); } @@ -37,12 +38,16 @@ function getDeepLToken() { } function performDeepLTranslation(token) { - cp.spawnSync('yarn', [ + const childProcess = cp.spawnSync('yarn', [ 'theia', 'nls-localize', '-f', './packages/core/i18n/nls.json', - '--free-api', '-k', token, - 'cs', 'de', 'es', 'fr', 'hu', 'it', 'ja', 'pl', 'pt-br', 'pt-pt', 'ru', 'zh-cn' + '--free-api', '-k', token ], { - shell: true + shell: true, + stdio: 'inherit' }); + if (childProcess.status !== 0) { + console.error('DeepL translation failed'); + process.exit(1); + } } diff --git a/tsconfig.json b/tsconfig.json index 732d0029c6500..9b9553dfdd5a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "./configs/base.tsconfig.json", - "include": [ ], + "include": [], "compilerOptions": { "composite": true, "allowJs": true @@ -36,24 +36,63 @@ { "path": "dev-packages/request" }, + { + "path": "examples/api-provider-sample" + }, { "path": "examples/api-samples" }, { "path": "examples/browser" }, + { + "path": "examples/browser-only" + }, { "path": "examples/electron" }, { "path": "examples/playwright" }, + { + "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" }, @@ -63,6 +102,9 @@ { "path": "packages/debug" }, + { + "path": "packages/dev-container" + }, { "path": "packages/editor" }, @@ -126,6 +168,9 @@ { "path": "packages/plugin-ext" }, + { + "path": "packages/plugin-ext-headless" + }, { "path": "packages/plugin-ext-vscode" }, diff --git a/yarn.lock b/yarn.lock index 4a9da1bdbf274..e3ca193777616 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,46 +22,46 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.13": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" - integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== dependencies: - "@babel/highlight" "^7.22.13" + "@babel/highlight" "^7.23.4" chalk "^2.4.2" -"@babel/compat-data@^7.22.20", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.20.tgz#8df6e96661209623f1975d66c35ffca66f3306d0" - integrity sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw== +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.3", "@babel/compat-data@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" + integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== "@babel/core@^7.10.0", "@babel/core@^7.7.5": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.0.tgz#f8259ae0e52a123eb40f552551e647b506a94d83" - integrity sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ== + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz#b028820718000f267870822fec434820e9b1e4d1" + integrity sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.0" - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-module-transforms" "^7.23.0" - "@babel/helpers" "^7.23.0" - "@babel/parser" "^7.23.0" - "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.0" - "@babel/types" "^7.23.0" + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.23.9" + "@babel/parser" "^7.23.9" + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" - integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== +"@babel/generator@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== dependencies: - "@babel/types" "^7.23.0" + "@babel/types" "^7.23.6" "@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" @@ -73,40 +73,40 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== dependencies: "@babel/types" "^7.22.15" -"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" - integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== +"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-validator-option" "^7.22.15" - browserslist "^4.21.9" + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" - integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg== +"@babel/helper-create-class-features-plugin@^7.22.15": + version "7.23.10" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz#25d55fafbaea31fd0e723820bb6cc3df72edf7ea" + integrity sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-member-expression-to-functions" "^7.23.0" "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-replace-supers" "^7.22.20" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== @@ -115,10 +115,10 @@ regexpu-core "^5.3.1" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" - integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== +"@babel/helper-define-polyfill-provider@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz#465805b7361f461e86c680f1de21eaf88c25901b" + integrity sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -126,7 +126,7 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": +"@babel/helper-environment-visitor@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== @@ -146,24 +146,24 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-member-expression-to-functions@^7.22.15": +"@babel/helper-member-expression-to-functions@^7.22.15", "@babel/helper-member-expression-to-functions@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== dependencies: "@babel/types" "^7.23.0" -"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": +"@babel/helper-module-imports@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== dependencies: "@babel/types" "^7.22.15" -"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e" - integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw== +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== dependencies: "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-module-imports" "^7.22.15" @@ -183,7 +183,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== -"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9": +"@babel/helper-remap-async-to-generator@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== @@ -192,7 +192,7 @@ "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-wrap-function" "^7.22.20" -"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": +"@babel/helper-replace-supers@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== @@ -222,20 +222,20 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-string-parser@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" - integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/helper-validator-option@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" - integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== "@babel/helper-wrap-function@^7.22.20": version "7.22.20" @@ -246,44 +246,52 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.22.19" -"@babel/helpers@^7.23.0": - version "7.23.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.1.tgz#44e981e8ce2b9e99f8f0b703f3326a4636c16d15" - integrity sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA== +"@babel/helpers@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" + integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== dependencies: - "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.0" - "@babel/types" "^7.23.0" + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.13": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" - integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== +"@babel/highlight@^7.10.4", "@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== dependencies: "@babel/helper-validator-identifier" "^7.22.20" chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" - integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== +"@babel/parser@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz#02dc8a03f613ed5fdc29fb2f728397c78146c962" - integrity sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a" + integrity sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz#2aeb91d337d4e1a1e7ce85b76a37f5301781200f" - integrity sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz#f6652bb16b94f8f9c20c50941e16e9756898dc5d" + integrity sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.22.15" + "@babel/plugin-transform-optional-chaining" "^7.23.3" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.23.7": + version "7.23.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz#516462a95d10a9618f197d39ad291a9b47ae1d7b" + integrity sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" @@ -325,17 +333,17 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-import-assertions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" - integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== +"@babel/plugin-syntax-import-assertions@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz#9c05a7f592982aff1a2768260ad84bcd3f0c77fc" + integrity sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-syntax-import-attributes@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" - integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== +"@babel/plugin-syntax-import-attributes@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz#992aee922cf04512461d7dae3ff6951b90a2dc06" + integrity sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -417,211 +425,211 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" - integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== +"@babel/plugin-transform-arrow-functions@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz#94c6dcfd731af90f27a79509f9ab7fb2120fc38b" + integrity sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-async-generator-functions@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz#3b153af4a6b779f340d5b80d3f634f55820aefa3" - integrity sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w== +"@babel/plugin-transform-async-generator-functions@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz#9adaeb66fc9634a586c5df139c6240d41ed801ce" + integrity sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.9" + "@babel/helper-remap-async-to-generator" "^7.22.20" "@babel/plugin-syntax-async-generators" "^7.8.4" -"@babel/plugin-transform-async-to-generator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" - integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== +"@babel/plugin-transform-async-to-generator@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz#d1f513c7a8a506d43f47df2bf25f9254b0b051fa" + integrity sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw== dependencies: - "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-module-imports" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.20" -"@babel/plugin-transform-block-scoped-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" - integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== +"@babel/plugin-transform-block-scoped-functions@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz#fe1177d715fb569663095e04f3598525d98e8c77" + integrity sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-block-scoping@^7.22.15": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz#8744d02c6c264d82e1a4bc5d2d501fd8aff6f022" - integrity sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g== +"@babel/plugin-transform-block-scoping@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz#b2d38589531c6c80fbe25e6b58e763622d2d3cf5" + integrity sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-class-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" - integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== +"@babel/plugin-transform-class-properties@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz#35c377db11ca92a785a718b6aa4e3ed1eb65dc48" + integrity sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-class-static-block@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974" - integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g== +"@babel/plugin-transform-class-static-block@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz#2a202c8787a8964dd11dfcedf994d36bfc844ab5" + integrity sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-create-class-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-transform-classes@^7.10.0", "@babel/plugin-transform-classes@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz#aaf4753aee262a232bbc95451b4bdf9599c65a0b" - integrity sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw== +"@babel/plugin-transform-classes@^7.10.0", "@babel/plugin-transform-classes@^7.23.8": + version "7.23.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz#d08ae096c240347badd68cdf1b6d1624a6435d92" + integrity sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-replace-supers" "^7.22.20" "@babel/helper-split-export-declaration" "^7.22.6" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" - integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== +"@babel/plugin-transform-computed-properties@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz#652e69561fcc9d2b50ba4f7ac7f60dcf65e86474" + integrity sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" - "@babel/template" "^7.22.5" + "@babel/template" "^7.22.15" -"@babel/plugin-transform-destructuring@^7.22.15": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz#6447aa686be48b32eaf65a73e0e2c0bd010a266c" - integrity sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg== +"@babel/plugin-transform-destructuring@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz#8c9ee68228b12ae3dff986e56ed1ba4f3c446311" + integrity sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-dotall-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" - integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== +"@babel/plugin-transform-dotall-regex@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz#3f7af6054882ede89c378d0cf889b854a993da50" + integrity sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-duplicate-keys@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" - integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== +"@babel/plugin-transform-duplicate-keys@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz#664706ca0a5dfe8d066537f99032fc1dc8b720ce" + integrity sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-dynamic-import@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa" - integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA== +"@babel/plugin-transform-dynamic-import@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz#c7629e7254011ac3630d47d7f34ddd40ca535143" + integrity sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" -"@babel/plugin-transform-exponentiation-operator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" - integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== +"@babel/plugin-transform-exponentiation-operator@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz#ea0d978f6b9232ba4722f3dbecdd18f450babd18" + integrity sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-export-namespace-from@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c" - integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw== +"@babel/plugin-transform-export-namespace-from@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz#084c7b25e9a5c8271e987a08cf85807b80283191" + integrity sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-transform-for-of@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz#f64b4ccc3a4f131a996388fae7680b472b306b29" - integrity sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA== +"@babel/plugin-transform-for-of@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz#81c37e24171b37b370ba6aaffa7ac86bcb46f94e" + integrity sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" -"@babel/plugin-transform-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" - integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== +"@babel/plugin-transform-function-name@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz#8f424fcd862bf84cb9a1a6b42bc2f47ed630f8dc" + integrity sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw== dependencies: - "@babel/helper-compilation-targets" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-function-name" "^7.23.0" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-json-strings@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835" - integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw== +"@babel/plugin-transform-json-strings@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz#a871d9b6bd171976efad2e43e694c961ffa3714d" + integrity sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-transform-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" - integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== +"@babel/plugin-transform-literals@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz#8214665f00506ead73de157eba233e7381f3beb4" + integrity sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-logical-assignment-operators@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c" - integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ== +"@babel/plugin-transform-logical-assignment-operators@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz#e599f82c51d55fac725f62ce55d3a0886279ecb5" + integrity sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" - integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== +"@babel/plugin-transform-member-expression-literals@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz#e37b3f0502289f477ac0e776b05a833d853cabcc" + integrity sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-modules-amd@^7.22.5": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz#05b2bc43373faa6d30ca89214731f76f966f3b88" - integrity sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw== +"@babel/plugin-transform-modules-amd@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz#e19b55436a1416829df0a1afc495deedfae17f7d" + integrity sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw== dependencies: - "@babel/helper-module-transforms" "^7.23.0" + "@babel/helper-module-transforms" "^7.23.3" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-modules-commonjs@^7.22.15": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz#b3dba4757133b2762c00f4f94590cf6d52602481" - integrity sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ== +"@babel/plugin-transform-modules-commonjs@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz#661ae831b9577e52be57dd8356b734f9700b53b4" + integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA== dependencies: - "@babel/helper-module-transforms" "^7.23.0" + "@babel/helper-module-transforms" "^7.23.3" "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-simple-access" "^7.22.5" -"@babel/plugin-transform-modules-systemjs@^7.22.11": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz#77591e126f3ff4132a40595a6cccd00a6b60d160" - integrity sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg== +"@babel/plugin-transform-modules-systemjs@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz#105d3ed46e4a21d257f83a2f9e2ee4203ceda6be" + integrity sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw== dependencies: "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.23.0" + "@babel/helper-module-transforms" "^7.23.3" "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-validator-identifier" "^7.22.20" -"@babel/plugin-transform-modules-umd@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" - integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== +"@babel/plugin-transform-modules-umd@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz#5d4395fccd071dfefe6585a4411aa7d6b7d769e9" + integrity sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg== dependencies: - "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.3" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": @@ -632,210 +640,211 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-new-target@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" - integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== +"@babel/plugin-transform-new-target@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz#5491bb78ed6ac87e990957cea367eab781c4d980" + integrity sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc" - integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg== +"@babel/plugin-transform-nullish-coalescing-operator@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz#45556aad123fc6e52189ea749e33ce090637346e" + integrity sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-transform-numeric-separator@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd" - integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg== +"@babel/plugin-transform-numeric-separator@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz#03d08e3691e405804ecdd19dd278a40cca531f29" + integrity sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-transform-object-rest-spread@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz#21a95db166be59b91cde48775310c0df6e1da56f" - integrity sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q== +"@babel/plugin-transform-object-rest-spread@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz#2b9c2d26bf62710460bdc0d1730d4f1048361b83" + integrity sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g== dependencies: - "@babel/compat-data" "^7.22.9" + "@babel/compat-data" "^7.23.3" "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.22.15" + "@babel/plugin-transform-parameters" "^7.23.3" -"@babel/plugin-transform-object-super@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" - integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== +"@babel/plugin-transform-object-super@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz#81fdb636dcb306dd2e4e8fd80db5b2362ed2ebcd" + integrity sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.20" -"@babel/plugin-transform-optional-catch-binding@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0" - integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ== +"@babel/plugin-transform-optional-catch-binding@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz#318066de6dacce7d92fa244ae475aa8d91778017" + integrity sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.22.15": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz#73ff5fc1cf98f542f09f29c0631647d8ad0be158" - integrity sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g== +"@babel/plugin-transform-optional-chaining@^7.23.3", "@babel/plugin-transform-optional-chaining@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz#6acf61203bdfc4de9d4e52e64490aeb3e52bd017" + integrity sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-transform-parameters@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz#719ca82a01d177af358df64a514d64c2e3edb114" - integrity sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ== +"@babel/plugin-transform-parameters@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz#83ef5d1baf4b1072fa6e54b2b0999a7b2527e2af" + integrity sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-private-methods@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" - integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== +"@babel/plugin-transform-private-methods@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz#b2d7a3c97e278bfe59137a978d53b2c2e038c0e4" + integrity sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-private-property-in-object@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1" - integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ== +"@babel/plugin-transform-private-property-in-object@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz#3ec711d05d6608fd173d9b8de39872d8dbf68bf5" + integrity sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-create-class-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" -"@babel/plugin-transform-property-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" - integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== +"@babel/plugin-transform-property-literals@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz#54518f14ac4755d22b92162e4a852d308a560875" + integrity sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-regenerator@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz#8ceef3bd7375c4db7652878b0241b2be5d0c3cca" - integrity sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw== +"@babel/plugin-transform-regenerator@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz#141afd4a2057298602069fce7f2dc5173e6c561c" + integrity sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" - integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== +"@babel/plugin-transform-reserved-words@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz#4130dcee12bd3dd5705c587947eb715da12efac8" + integrity sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-transform-runtime@^7.10.0": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.15.tgz#3a625c4c05a39e932d7d34f5d4895cdd0172fdc9" - integrity sha512-tEVLhk8NRZSmwQ0DJtxxhTrCht1HVo8VaMzYT4w6lwyKBuHsgoioAUA7/6eT2fRfc5/23fuGdlwIxXhRVgWr4g== + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz#2c64d0680fc8e09e1dfe8fd5c646fe72abd82004" + integrity sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ== dependencies: "@babel/helper-module-imports" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" - babel-plugin-polyfill-corejs2 "^0.4.5" - babel-plugin-polyfill-corejs3 "^0.8.3" - babel-plugin-polyfill-regenerator "^0.5.2" + babel-plugin-polyfill-corejs2 "^0.4.8" + babel-plugin-polyfill-corejs3 "^0.9.0" + babel-plugin-polyfill-regenerator "^0.5.5" semver "^6.3.1" -"@babel/plugin-transform-shorthand-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" - integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== +"@babel/plugin-transform-shorthand-properties@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz#97d82a39b0e0c24f8a981568a8ed851745f59210" + integrity sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-spread@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" - integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== +"@babel/plugin-transform-spread@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz#41d17aacb12bde55168403c6f2d6bdca563d362c" + integrity sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" -"@babel/plugin-transform-sticky-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" - integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== +"@babel/plugin-transform-sticky-regex@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz#dec45588ab4a723cb579c609b294a3d1bd22ff04" + integrity sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-template-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" - integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== +"@babel/plugin-transform-template-literals@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz#5f0f028eb14e50b5d0f76be57f90045757539d07" + integrity sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-typeof-symbol@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" - integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== +"@babel/plugin-transform-typeof-symbol@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz#9dfab97acc87495c0c449014eb9c547d8966bca4" + integrity sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-unicode-escapes@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9" - integrity sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg== +"@babel/plugin-transform-unicode-escapes@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz#1f66d16cab01fab98d784867d24f70c1ca65b925" + integrity sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-unicode-property-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" - integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== +"@babel/plugin-transform-unicode-property-regex@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz#19e234129e5ffa7205010feec0d94c251083d7ad" + integrity sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-unicode-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" - integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== +"@babel/plugin-transform-unicode-regex@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz#26897708d8f42654ca4ce1b73e96140fbad879dc" + integrity sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-unicode-sets-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" - integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== +"@babel/plugin-transform-unicode-sets-regex@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz#4fb6f0a719c2c5859d11f6b55a050cc987f3799e" + integrity sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" "@babel/preset-env@^7.10.0": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.20.tgz#de9e9b57e1127ce0a2f580831717f7fb677ceedb" - integrity sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg== + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.9.tgz#beace3b7994560ed6bf78e4ae2073dff45387669" + integrity sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A== dependencies: - "@babel/compat-data" "^7.22.20" - "@babel/helper-compilation-targets" "^7.22.15" + "@babel/compat-data" "^7.23.5" + "@babel/helper-compilation-targets" "^7.23.6" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.15" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.15" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.15" + "@babel/helper-validator-option" "^7.23.5" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.23.3" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.23.3" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.23.7" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.22.5" - "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-assertions" "^7.23.3" + "@babel/plugin-syntax-import-attributes" "^7.23.3" "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" @@ -847,59 +856,58 @@ "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.22.5" - "@babel/plugin-transform-async-generator-functions" "^7.22.15" - "@babel/plugin-transform-async-to-generator" "^7.22.5" - "@babel/plugin-transform-block-scoped-functions" "^7.22.5" - "@babel/plugin-transform-block-scoping" "^7.22.15" - "@babel/plugin-transform-class-properties" "^7.22.5" - "@babel/plugin-transform-class-static-block" "^7.22.11" - "@babel/plugin-transform-classes" "^7.22.15" - "@babel/plugin-transform-computed-properties" "^7.22.5" - "@babel/plugin-transform-destructuring" "^7.22.15" - "@babel/plugin-transform-dotall-regex" "^7.22.5" - "@babel/plugin-transform-duplicate-keys" "^7.22.5" - "@babel/plugin-transform-dynamic-import" "^7.22.11" - "@babel/plugin-transform-exponentiation-operator" "^7.22.5" - "@babel/plugin-transform-export-namespace-from" "^7.22.11" - "@babel/plugin-transform-for-of" "^7.22.15" - "@babel/plugin-transform-function-name" "^7.22.5" - "@babel/plugin-transform-json-strings" "^7.22.11" - "@babel/plugin-transform-literals" "^7.22.5" - "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" - "@babel/plugin-transform-member-expression-literals" "^7.22.5" - "@babel/plugin-transform-modules-amd" "^7.22.5" - "@babel/plugin-transform-modules-commonjs" "^7.22.15" - "@babel/plugin-transform-modules-systemjs" "^7.22.11" - "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-arrow-functions" "^7.23.3" + "@babel/plugin-transform-async-generator-functions" "^7.23.9" + "@babel/plugin-transform-async-to-generator" "^7.23.3" + "@babel/plugin-transform-block-scoped-functions" "^7.23.3" + "@babel/plugin-transform-block-scoping" "^7.23.4" + "@babel/plugin-transform-class-properties" "^7.23.3" + "@babel/plugin-transform-class-static-block" "^7.23.4" + "@babel/plugin-transform-classes" "^7.23.8" + "@babel/plugin-transform-computed-properties" "^7.23.3" + "@babel/plugin-transform-destructuring" "^7.23.3" + "@babel/plugin-transform-dotall-regex" "^7.23.3" + "@babel/plugin-transform-duplicate-keys" "^7.23.3" + "@babel/plugin-transform-dynamic-import" "^7.23.4" + "@babel/plugin-transform-exponentiation-operator" "^7.23.3" + "@babel/plugin-transform-export-namespace-from" "^7.23.4" + "@babel/plugin-transform-for-of" "^7.23.6" + "@babel/plugin-transform-function-name" "^7.23.3" + "@babel/plugin-transform-json-strings" "^7.23.4" + "@babel/plugin-transform-literals" "^7.23.3" + "@babel/plugin-transform-logical-assignment-operators" "^7.23.4" + "@babel/plugin-transform-member-expression-literals" "^7.23.3" + "@babel/plugin-transform-modules-amd" "^7.23.3" + "@babel/plugin-transform-modules-commonjs" "^7.23.3" + "@babel/plugin-transform-modules-systemjs" "^7.23.9" + "@babel/plugin-transform-modules-umd" "^7.23.3" "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" - "@babel/plugin-transform-new-target" "^7.22.5" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" - "@babel/plugin-transform-numeric-separator" "^7.22.11" - "@babel/plugin-transform-object-rest-spread" "^7.22.15" - "@babel/plugin-transform-object-super" "^7.22.5" - "@babel/plugin-transform-optional-catch-binding" "^7.22.11" - "@babel/plugin-transform-optional-chaining" "^7.22.15" - "@babel/plugin-transform-parameters" "^7.22.15" - "@babel/plugin-transform-private-methods" "^7.22.5" - "@babel/plugin-transform-private-property-in-object" "^7.22.11" - "@babel/plugin-transform-property-literals" "^7.22.5" - "@babel/plugin-transform-regenerator" "^7.22.10" - "@babel/plugin-transform-reserved-words" "^7.22.5" - "@babel/plugin-transform-shorthand-properties" "^7.22.5" - "@babel/plugin-transform-spread" "^7.22.5" - "@babel/plugin-transform-sticky-regex" "^7.22.5" - "@babel/plugin-transform-template-literals" "^7.22.5" - "@babel/plugin-transform-typeof-symbol" "^7.22.5" - "@babel/plugin-transform-unicode-escapes" "^7.22.10" - "@babel/plugin-transform-unicode-property-regex" "^7.22.5" - "@babel/plugin-transform-unicode-regex" "^7.22.5" - "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.23.3" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.23.4" + "@babel/plugin-transform-numeric-separator" "^7.23.4" + "@babel/plugin-transform-object-rest-spread" "^7.23.4" + "@babel/plugin-transform-object-super" "^7.23.3" + "@babel/plugin-transform-optional-catch-binding" "^7.23.4" + "@babel/plugin-transform-optional-chaining" "^7.23.4" + "@babel/plugin-transform-parameters" "^7.23.3" + "@babel/plugin-transform-private-methods" "^7.23.3" + "@babel/plugin-transform-private-property-in-object" "^7.23.4" + "@babel/plugin-transform-property-literals" "^7.23.3" + "@babel/plugin-transform-regenerator" "^7.23.3" + "@babel/plugin-transform-reserved-words" "^7.23.3" + "@babel/plugin-transform-shorthand-properties" "^7.23.3" + "@babel/plugin-transform-spread" "^7.23.3" + "@babel/plugin-transform-sticky-regex" "^7.23.3" + "@babel/plugin-transform-template-literals" "^7.23.3" + "@babel/plugin-transform-typeof-symbol" "^7.23.3" + "@babel/plugin-transform-unicode-escapes" "^7.23.3" + "@babel/plugin-transform-unicode-property-regex" "^7.23.3" + "@babel/plugin-transform-unicode-regex" "^7.23.3" + "@babel/plugin-transform-unicode-sets-regex" "^7.23.3" "@babel/preset-modules" "0.1.6-no-external-plugins" - "@babel/types" "^7.22.19" - babel-plugin-polyfill-corejs2 "^0.4.5" - babel-plugin-polyfill-corejs3 "^0.8.3" - babel-plugin-polyfill-regenerator "^0.5.2" + babel-plugin-polyfill-corejs2 "^0.4.8" + babel-plugin-polyfill-corejs3 "^0.9.0" + babel-plugin-polyfill-regenerator "^0.5.5" core-js-compat "^3.31.0" semver "^6.3.1" @@ -918,46 +926,51 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime@^7.10.0", "@babel/runtime@^7.8.4": - version "7.23.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d" - integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g== + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" + integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.22.15", "@babel/template@^7.22.5": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" - integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== +"@babel/template@^7.22.15", "@babel/template@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" + integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/parser" "^7.22.15" - "@babel/types" "^7.22.15" + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" -"@babel/traverse@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.0.tgz#18196ddfbcf4ccea324b7f6d3ada00d8c5a99c53" - integrity sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw== +"@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.0" + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.0" - "@babel/types" "^7.23.0" - debug "^4.1.0" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.4.4": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" - integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== +"@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.23.9", "@babel/types@^7.4.4": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" + integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== dependencies: - "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-string-parser" "^7.23.4" "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@balena/dockerignore@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" + integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -978,6 +991,18 @@ optionalDependencies: global-agent "^3.0.0" +"@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -993,6 +1018,11 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@gar/promisify@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -1048,18 +1078,18 @@ "@sinclair/typebox" "^0.27.8" "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" - integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + version "0.3.4" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz#9b18145d26cf33d08576cf4c7665b28554480ed7" + integrity sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw== dependencies: "@jridgewell/set-array" "^1.0.1" "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/resolve-uri@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/set-array@^1.0.1": version "1.1.2" @@ -1079,29 +1109,29 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.19" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" - integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.23" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz#afc96847f3f07841477f303eed687707a5aacd80" + integrity sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@lerna/child-process@7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-7.3.0.tgz#c56488a8a881f22a64793bf9339c5a2450a18559" - integrity sha512-rA+fGUo2j/LEq6w1w8s6oVikLbJTWoIDVpYMc7bUCtwDOUuZKMQiRtjmpavY3fTm7ltu42f4AKflc2A70K4wvA== +"@lerna/child-process@7.4.2": + version "7.4.2" + resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-7.4.2.tgz#a2fd013ac2150dc288270d3e0d0b850c06bec511" + integrity sha512-je+kkrfcvPcwL5Tg8JRENRqlbzjdlZXyaR88UcnCdNW0AJ1jX9IfHRys1X7AwSroU2ug8ESNC+suoBw1vX833Q== dependencies: chalk "^4.1.0" execa "^5.0.0" strong-log-transformer "^2.1.0" -"@lerna/create@7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@lerna/create/-/create-7.3.0.tgz#5438c231f617b8e825731390d394f8684af471d5" - integrity sha512-fjgiKjg9VXwQ4ZKKsrXICEKRiC3yo6+FprR0mc55uz0s5e9xupoSGLobUTTBdE7ncNB3ibqml8dfaAn/+ESajQ== +"@lerna/create@7.4.2": + version "7.4.2" + resolved "https://registry.yarnpkg.com/@lerna/create/-/create-7.4.2.tgz#f845fad1480e46555af98bd39af29571605dddc9" + integrity sha512-1wplFbQ52K8E/unnqB0Tq39Z4e+NEoNrpovEnl6GpsTUrC6WDp8+w0Le2uCBV0hXyemxChduCkLz4/y1H1wTeg== dependencies: - "@lerna/child-process" "7.3.0" + "@lerna/child-process" "7.4.2" "@npmcli/run-script" "6.0.2" "@nx/devkit" ">=16.5.1 < 17" "@octokit/plugin-enterprise-rest" "6.0.1" @@ -1174,35 +1204,35 @@ dependencies: cross-spawn "^7.0.1" -"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.2.0.tgz#901c5937e1441572ea23e631fe6deca68482fe76" - integrity sha512-Z9LFPzfoJi4mflGWV+rv7o7ZbMU5oAU9VmzCgL240KnqDW65Y2HFCT3MW06/ITJSnbVLacmcEJA8phywK7JinQ== +"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38" + integrity sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ== -"@msgpackr-extract/msgpackr-extract-darwin-x64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.2.0.tgz#fb877fe6bae3c4d3cea29786737840e2ae689066" - integrity sha512-vq0tT8sjZsy4JdSqmadWVw6f66UXqUCabLmUVHZwUFzMgtgoIIQjT4VVRHKvlof3P/dMCkbMJ5hB1oJ9OWHaaw== +"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz#f954f34355712212a8e06c465bc06c40852c6bb3" + integrity sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw== -"@msgpackr-extract/msgpackr-extract-linux-arm64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.2.0.tgz#986179c38b10ac41fbdaf7d036c825cbc72855d9" - integrity sha512-hlxxLdRmPyq16QCutUtP8Tm6RDWcyaLsRssaHROatgnkOxdleMTgetf9JsdncL8vLh7FVy/RN9i3XR5dnb9cRA== +"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz#45c63037f045c2b15c44f80f0393fa24f9655367" + integrity sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg== -"@msgpackr-extract/msgpackr-extract-linux-arm@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.2.0.tgz#15f2c6fe9e0adc06c21af7e95f484ff4880d79ce" - integrity sha512-SaJ3Qq4lX9Syd2xEo9u3qPxi/OB+5JO/ngJKK97XDpa1C587H9EWYO6KD8995DAjSinWvdHKRrCOXVUC5fvGOg== +"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz#35707efeafe6d22b3f373caf9e8775e8920d1399" + integrity sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA== -"@msgpackr-extract/msgpackr-extract-linux-x64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.2.0.tgz#30cae5c9a202f3e1fa1deb3191b18ffcb2f239a2" - integrity sha512-94y5PJrSOqUNcFKmOl7z319FelCLAE0rz/jPCWS+UtdMZvpa4jrQd+cJPQCLp2Fes1yAW/YUQj/Di6YVT3c3Iw== +"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz#091b1218b66c341f532611477ef89e83f25fae4f" + integrity sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA== -"@msgpackr-extract/msgpackr-extract-win32-x64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.2.0.tgz#016d855b6bc459fd908095811f6826e45dd4ba64" - integrity sha512-XrC0JzsqQSvOyM3t04FMLO6z5gCuhPE6k4FXuLK5xf52ZbdvcFe1yBmo7meCew9B8G2f0T9iu9t3kfTYRYROgA== +"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz#0f164b726869f71da3c594171df5ebc1c4b0a407" + integrity sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1225,6 +1255,14 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@npmcli/fs@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" + integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== + dependencies: + "@gar/promisify" "^1.1.3" + semver "^7.3.5" + "@npmcli/fs@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.0.tgz#233d43a25a91d68c3a863ba0da6a3f00924a173e" @@ -1254,6 +1292,14 @@ npm-bundled "^3.0.0" npm-normalize-package-bin "^3.0.0" +"@npmcli/move-file@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" + integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@npmcli/node-gyp@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz#101b2d0490ef1aa20ed460e4c0813f0db560545a" @@ -1471,6 +1517,66 @@ dependencies: "@octokit/openapi-types" "^18.0.0" +"@parcel/watcher-android-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84" + integrity sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg== + +"@parcel/watcher-darwin-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz#c817c7a3b4f3a79c1535bfe54a1c2818d9ffdc34" + integrity sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA== + +"@parcel/watcher-darwin-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz#1a3f69d9323eae4f1c61a5f480a59c478d2cb020" + integrity sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg== + +"@parcel/watcher-freebsd-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz#0d67fef1609f90ba6a8a662bc76a55fc93706fc8" + integrity sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w== + +"@parcel/watcher-linux-arm-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz#ce5b340da5829b8e546bd00f752ae5292e1c702d" + integrity sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA== + +"@parcel/watcher-linux-arm64-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz#6d7c00dde6d40608f9554e73998db11b2b1ff7c7" + integrity sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA== + +"@parcel/watcher-linux-arm64-musl@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz#bd39bc71015f08a4a31a47cd89c236b9d6a7f635" + integrity sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA== + +"@parcel/watcher-linux-x64-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz#0ce29966b082fb6cdd3de44f2f74057eef2c9e39" + integrity sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg== + +"@parcel/watcher-linux-x64-musl@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz#d2ebbf60e407170bb647cd6e447f4f2bab19ad16" + integrity sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ== + +"@parcel/watcher-win32-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz#eb4deef37e80f0b5e2f215dd6d7a6d40a85f8adc" + integrity sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg== + +"@parcel/watcher-win32-ia32@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz#94fbd4b497be39fd5c8c71ba05436927842c9df7" + integrity sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw== + +"@parcel/watcher-win32-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz#4bf920912f67cae5f2d264f58df81abfea68dadf" + integrity sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A== + "@parcel/watcher@2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" @@ -1479,6 +1585,29 @@ node-addon-api "^3.2.1" node-gyp-build "^4.3.0" +"@parcel/watcher@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.4.1.tgz#a50275151a1bb110879c6123589dba90c19f1bf8" + integrity sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.4.1" + "@parcel/watcher-darwin-arm64" "2.4.1" + "@parcel/watcher-darwin-x64" "2.4.1" + "@parcel/watcher-freebsd-x64" "2.4.1" + "@parcel/watcher-linux-arm-glibc" "2.4.1" + "@parcel/watcher-linux-arm64-glibc" "2.4.1" + "@parcel/watcher-linux-arm64-musl" "2.4.1" + "@parcel/watcher-linux-x64-glibc" "2.4.1" + "@parcel/watcher-linux-x64-musl" "2.4.1" + "@parcel/watcher-win32-arm64" "2.4.1" + "@parcel/watcher-win32-ia32" "2.4.1" + "@parcel/watcher-win32-x64" "2.4.1" + "@phosphor/algorithm@1", "@phosphor/algorithm@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@phosphor/algorithm/-/algorithm-1.2.0.tgz#4a19aa59261b7270be696672dc3f0663f7bef152" @@ -1584,11 +1713,25 @@ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@playwright/test@^1.37.1": - version "1.38.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.38.1.tgz#8ef4263e355cd1d8ad7905d471d268e8acb82ed6" - integrity sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ== + version "1.41.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.41.2.tgz#bd9db40177f8fd442e16e14e0389d23751cdfc54" + integrity sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg== dependencies: - playwright "1.38.1" + playwright "1.41.2" + +"@puppeteer/browsers@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.3.1.tgz#238200dbdce5c00ae28c8f2a55ac053c3be71668" + integrity sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA== + dependencies: + debug "^4.3.6" + extract-zip "^2.0.1" + progress "^2.0.3" + proxy-agent "^6.4.0" + semver "^7.6.3" + tar-fs "^3.0.6" + unbzip2-stream "^1.4.3" + yargs "^17.7.2" "@sigstore/bundle@^1.1.0": version "1.1.0" @@ -1648,24 +1791,17 @@ dependencies: type-detect "4.0.8" -"@sinonjs/commons@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" - integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== - dependencies: - type-detect "4.0.8" - "@sinonjs/commons@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" - integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^10.0.2": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" - integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== +"@sinonjs/fake-timers@^11.2.2": + version "11.2.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz#50063cc3574f4a27bd8453180a04171c85cc9699" + integrity sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw== dependencies: "@sinonjs/commons" "^3.0.0" @@ -1685,7 +1821,7 @@ lodash.get "^4.4.2" type-detect "^4.0.8" -"@sinonjs/text-encoding@^0.7.1": +"@sinonjs/text-encoding@^0.7.2": version "0.7.2" resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== @@ -1707,10 +1843,10 @@ dependencies: defer-to-connect "^2.0.0" -"@theia/monaco-editor-core@1.72.3": - version "1.72.3" - resolved "https://registry.yarnpkg.com/@theia/monaco-editor-core/-/monaco-editor-core-1.72.3.tgz#911d674c6e0c490442a355cfaa52beec919a025e" - integrity sha512-2FK5m0G5oxiqCv0ZrjucMx5fVgQ9Jqv0CgxGvSzDc4wRrauBdeBoX90J99BEIOJ8Jp3W0++GoRBdh0yQNIGL2g== +"@theia/monaco-editor-core@1.83.101": + version "1.83.101" + resolved "https://registry.yarnpkg.com/@theia/monaco-editor-core/-/monaco-editor-core-1.83.101.tgz#a0577396fb4c69540536df2d7fed2de4399c4fde" + integrity sha512-UaAi6CEvain/qbGD3o6Ufe8plLyzAVQ53p9Ke+MoBYDhb391+r+MuK++JtITqIrXqoa8OCjbt8wQxEFSNNO0Mw== "@tootallnate/once@1", "@tootallnate/once@^1.1.2": version "1.1.2" @@ -1722,6 +1858,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@tootallnate/quickjs-emscripten@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" + integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== + "@tufjs/canonical-json@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz#eade9fd1f537993bc1f0949f3aea276ecc4fab31" @@ -1736,23 +1877,23 @@ minimatch "^9.0.0" "@types/archiver@^5.3.2": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.3.tgz#9cb632a67060602b1658c669b498d51dd8ce08ab" - integrity sha512-0ABdVcXL6jOwNGY+hjWPqrxUvKelBEwNLcuv/SV2vZ4YCH8w9NttFCt+/QqI5zgMX+iX/XqVy89/r7EmLJmMpQ== + version "5.3.4" + resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.4.tgz#32172d5a56f165b5b4ac902e366248bf03d3ae84" + integrity sha512-Lj7fLBIMwYFgViVVZHEdExZC3lVYsl+QL0VmdNdIzGZH544jHveYWij6qdnBgJQDnR7pMKliN9z2cPZFEbhyPw== dependencies: "@types/readdir-glob" "*" "@types/bent@^7.0.1": - version "7.3.5" - resolved "https://registry.yarnpkg.com/@types/bent/-/bent-7.3.5.tgz#0676776c1ea70bed464234435b80a6acbc8d9c7d" - integrity sha512-7PTYvy4UERqRPwlz/2KMXyCu08JpvN+SHBOH1Kzp+haZFsX1xrC+RI5qFVERTIDp1XoA+VnfatRmSM7x/0p3vw== + version "7.3.8" + resolved "https://registry.yarnpkg.com/@types/bent/-/bent-7.3.8.tgz#69c3ee49bf6593d831006794e7bd2f84bb573e58" + integrity sha512-yZ09JA1KsA5Fl6Oh/ahK00+H5bV0qCy2bYnyfiFY42wnaMK4n7IDC6HaFe3WW45Zhnak7iqJBKlWD0nVxzrGWg== dependencies: "@types/node" "*" "@types/body-parser@*", "@types/body-parser@^1.16.4", "@types/body-parser@^1.17.0": - version "1.19.3" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.3.tgz#fb558014374f7d9e56c8f34bab2042a3a07d25cd" - integrity sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ== + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== dependencies: "@types/connect" "*" "@types/node" "*" @@ -1775,16 +1916,16 @@ "@types/chai" "*" "@types/chai-string@^1.4.0": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@types/chai-string/-/chai-string-1.4.3.tgz#06e02d74deed77c2bfccccae44ece6e57a8ecedd" - integrity sha512-bLp5xMQ7Ml0fWa05IPpLjIznTkNbuBE3GtRTzKrp0d10IavlBFcu9vXP2liWaXta79unO693q3kuRxD7g2YYGw== + version "1.4.5" + resolved "https://registry.yarnpkg.com/@types/chai-string/-/chai-string-1.4.5.tgz#988ff37526386e9c354219b163d7ecf81bab8d2d" + integrity sha512-IecXRMSnpUvRnTztdpSdjcmcW7EdNme65bfDCQMi7XrSEPGmyDYYTEfc5fcactWDA6ioSm8o7NUqg9QxjBCCEw== dependencies: "@types/chai" "*" "@types/chai@*", "@types/chai@^4.2.7": - version "4.3.7" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.7.tgz#5457bc3dce72f20ae533366682a6298471d1c610" - integrity sha512-/k+vesl92vMvMygmQrFe9Aimxi6oQXFUX9mA5HanTrKUSAMoLauSi6PNFOdRw0oeqilaW600GNx2vSaT2f8aIQ== + version "4.3.12" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.12.tgz#b192fe1c553b54f45d20543adc2ab88455a07d5e" + integrity sha512-zNKDHG/1yxm8Il6uCCVsm+dRdEsJlFoDu73X17y09bId6UwoYww+vFBsAcRzl8knM1sab3Dp1VRikFQwDOtDDw== "@types/chai@4.3.0": version "4.3.0" @@ -1792,9 +1933,9 @@ integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw== "@types/connect@*": - version "3.4.36" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.36.tgz#e511558c15a39cb29bd5357eebb57bd1459cd1ab" - integrity sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w== + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== dependencies: "@types/node" "*" @@ -1809,23 +1950,39 @@ integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== "@types/cors@^2.8.12": - version "2.8.14" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.14.tgz#94eeb1c95eda6a8ab54870a3bf88854512f43a92" - integrity sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ== + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== dependencies: "@types/node" "*" "@types/decompress@^4.2.2", "@types/decompress@^4.2.4": - version "4.2.5" - resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.5.tgz#07ed5b350303b945017692e87a653a09df166915" - integrity sha512-LdL+kbcKGs9TzvB/K+OBGzPfDoP6gwwTsykYjodlzUJUUYp/43c1p1jE5YTtz3z4Ml90iruvBXbJ6+kDvb3WSQ== + version "4.2.7" + resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.7.tgz#604f69b69d519ecb74dea1ea0829f159b85e1332" + integrity sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g== dependencies: "@types/node" "*" -"@types/diff@^3.2.2": - version "3.5.6" - resolved "https://registry.yarnpkg.com/@types/diff/-/diff-3.5.6.tgz#2524928a13888cebb59dc18e0c793022e7d02dfd" - integrity sha512-5BV7iGX/NmFGqAQn+YDBK++kO7IbZf0mIn8mwdJACIpZsMUqJvEin0riqNDbmS3SQL8u00dGnbC0FFJQptTSWw== +"@types/diff@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.2.1.tgz#cceae9c4b2dae5c6b8ab1ce1263601c255d87fb3" + integrity sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g== + +"@types/docker-modem@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/docker-modem/-/docker-modem-3.0.6.tgz#1f9262fcf85425b158ca725699a03eb23cddbf87" + integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg== + dependencies: + "@types/node" "*" + "@types/ssh2" "*" + +"@types/dockerode@^3.3.23": + version "3.3.23" + resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.23.tgz#07b2084013d01e14d5d97856446f4d9c9f27c223" + integrity sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw== + dependencies: + "@types/docker-modem" "*" + "@types/node" "*" "@types/dompurify@^2.2.2": version "2.4.0" @@ -1840,47 +1997,47 @@ integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw== "@types/eslint-scope@^3.7.3": - version "3.7.5" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.5.tgz#e28b09dbb1d9d35fdfa8a884225f00440dfc5a3e" - integrity sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA== + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== dependencies: "@types/eslint" "*" "@types/estree" "*" "@types/eslint@*": - version "8.44.3" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.3.tgz#96614fae4875ea6328f56de38666f582d911d962" - integrity sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g== + version "8.56.3" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.3.tgz#d1f6b2303ac5ed53cb2cf59e0ab680cde1698f5f" + integrity sha512-PvSf1wfv2wJpVIFUMSb+i4PvqNYkB9Rkp9ZDO3oaWzq4SKhsQk4mrMBr3ZH06I0hKrVGLBacmgl8JM4WVjb9dg== dependencies: "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.2.tgz#ff02bc3dc8317cd668dfec247b750ba1f1d62453" - integrity sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA== +"@types/estree@*", "@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"@types/express-http-proxy@^1.6.3": - version "1.6.4" - resolved "https://registry.yarnpkg.com/@types/express-http-proxy/-/express-http-proxy-1.6.4.tgz#42917facb194ab476c06f381838f211f4717a7dc" - integrity sha512-V0THpGPqxR85uHARStjYSKObI7ett4qA1JtiRqv/rv7pAt8IYFCtieLeq0GPnVYeR1BghgGQYlEZK7JPMUPrDQ== +"@types/express-http-proxy@^1.6.6": + version "1.6.6" + resolved "https://registry.yarnpkg.com/@types/express-http-proxy/-/express-http-proxy-1.6.6.tgz#386c6f4c61a2d26ab8817ba1c2b2aac80e5638c9" + integrity sha512-J8ZqHG76rq1UB716IZ3RCmUhg406pbWxsM3oFCFccl5xlWUPzoR4if6Og/cE4juK8emH0H9quZa5ltn6ZdmQJg== dependencies: "@types/express" "*" "@types/express-serve-static-core@^4.17.33": - version "4.17.37" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz#7e4b7b59da9142138a2aaa7621f5abedce8c7320" - integrity sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg== + version "4.17.43" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" + integrity sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" "@types/send" "*" -"@types/express@*", "@types/express@^4.16.0": - version "4.17.18" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.18.tgz#efabf5c4495c1880df1bdffee604b143b29c4a95" - integrity sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ== +"@types/express@*", "@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" @@ -1888,9 +2045,9 @@ "@types/serve-static" "*" "@types/fs-extra@^4.0.2": - version "4.0.13" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.13.tgz#0499ef6ab6dd1c9e05c5247383867a47f5929e0b" - integrity sha512-rMZ7c4t5/EQc2FD7OTbS5XPHCR4hUSVwkiTN0/CXaLDTwxE3IPNMrCKEroLDSYB0K7UTpEH6TAcN30ff+MJw9w== + version "4.0.15" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.15.tgz#82d70b4a2e5e3dd17474ce9e8fe951a03aeddd31" + integrity sha512-zU/EU2kZ1tv+p4pswQLntA7dFQq84wXrSCfmLjZvMbLjf4N46cPOWHg+WKfc27YnEOQ0chVFlBui55HRsvzHPA== dependencies: "@types/node" "*" @@ -1901,7 +2058,7 @@ dependencies: "@types/node" "*" -"@types/glob@*", "@types/glob@^8.1.0": +"@types/glob@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w== @@ -1917,28 +2074,33 @@ highlight.js "*" "@types/http-cache-semantics@*": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz#abe102d06ccda1efdf0ed98c10ccf7f36a785a41" - integrity sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw== + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== "@types/http-errors@*": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.2.tgz#a86e00bbde8950364f8e7846687259ffcd96e8c2" - integrity sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg== + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== -"@types/jsdom@^21.1.1": - version "21.1.3" - resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.3.tgz#a88c5dc65703e1b10b2a7839c12db49662b43ff0" - integrity sha512-1zzqSP+iHJYV4lB3lZhNBa012pubABkj9yG/GuXuf6LZH1cSPIJBqFDrm5JX65HHt6VOnNYdTui/0ySerRbMgA== +"@types/js-yaml@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + +"@types/jsdom@^21.1.7": + version "21.1.7" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.7.tgz#9edcb09e0b07ce876e7833922d3274149c898cfa" + integrity sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA== dependencies: "@types/node" "*" "@types/tough-cookie" "*" parse5 "^7.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.13" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" - integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== +"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/json5@^0.0.29": version "0.0.29" @@ -1953,14 +2115,14 @@ "@types/node" "*" "@types/linkify-it@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.3.tgz#15a0712296c5041733c79efe233ba17ae5a7587b" - integrity sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.5.tgz#1e78a3ac2428e6d7e6c05c1665c242023a4601d8" + integrity sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw== "@types/lodash.clonedeep@^4.5.3": - version "4.5.7" - resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.7.tgz#0e119f582ed6f9e6b373c04a644651763214f197" - integrity sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw== + version "4.5.9" + resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz#ea48276c7cc18d080e00bb56cf965bcceb3f0fc1" + integrity sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q== dependencies: "@types/lodash" "*" @@ -1972,16 +2134,16 @@ "@types/lodash" "*" "@types/lodash.throttle@^4.1.3": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz#4ef379eb4f778068022310ef166625f420b6ba58" - integrity sha512-znwGDpjCHQ4FpLLx19w4OXDqq8+OvREa05H89obtSyXyOFKL3dDjCslsmfBz0T2FU8dmf5Wx1QvogbINiGIu9g== + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz#f17a6ae084f7c0117bd7df145b379537bc9615c5" + integrity sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g== dependencies: "@types/lodash" "*" "@types/lodash@*": - version "4.14.199" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.199.tgz#c3edb5650149d847a277a8961a7ad360c474e9bf" - integrity sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg== + version "4.14.202" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" + integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== "@types/long@^4.0.0": version "4.0.2" @@ -2001,9 +2163,9 @@ "@types/markdown-it" "*" "@types/markdown-it@*": - version "13.0.2" - resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-13.0.2.tgz#1557e77789fc86e93fd4b8f0f8f8535ec97a8518" - integrity sha512-Tla7hH9oeXHOlJyBFdoqV61xWE9FZf/y2g+gFVwQ2vE1/eBzjUno5JCd3Hdb5oATve5OF6xNjZ/4VIZhVVx+hA== + version "13.0.7" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-13.0.7.tgz#4a495115f470075bd4434a0438ac477a49c2e152" + integrity sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA== dependencies: "@types/linkify-it" "*" "@types/mdurl" "*" @@ -2017,24 +2179,24 @@ "@types/mdurl" "*" "@types/mdurl@*": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.3.tgz#d0aefccdd1a96f4bec76047d6b314601f0b0f3de" - integrity sha512-T5k6kTXak79gwmIOaDF2UUQXFbnBE0zBUzF20pz7wDYu0RQMzWg+Ml/Pz50214NsFHBITkoi5VtdjFZnJ2ijjA== + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.5.tgz#3e0d2db570e9fb6ccb2dc8fde0be1d79ac810d39" + integrity sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA== "@types/mime-types@^2.1.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.2.tgz#b4fe6996d2f32975b6603b26b4e4b3b6c92c9901" - integrity sha512-q9QGHMGCiBJCHEvd4ZLdasdqXv570agPsUW0CeIm/B8DzhxsYMerD0l3IlI+EQ1A2RWHY2mmM9x1YIuuWxisCg== + version "2.1.4" + resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2" + integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w== "@types/mime@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.2.tgz#c1ae807f13d308ee7511a5b81c74f327028e66e8" - integrity sha512-Wj+fqpTLtTbG7c0tH47dkahefpLKEbB+xAZuLq7b4/IDHPl/n6VoXcyUQ2bypFlbSwvCr0y+bD4euTTqTJsPxQ== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45" + integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw== "@types/mime@^1": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.3.tgz#bbe64987e0eb05de150c305005055c7ad784a9ce" - integrity sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg== + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/mime@^2.0.1": version "2.0.3" @@ -2052,51 +2214,51 @@ integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== "@types/minimist@^1.2.0": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.3.tgz#dd249cef80c6fff2ba6a0d4e5beca913e04e25f8" - integrity sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/mocha@^10.0.0": - version "10.0.2" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.2.tgz#96d63314255540a36bf24da094cce7a13668d73b" - integrity sha512-NaHL0+0lLNhX6d9rs+NSt97WH/gIlRHmszXbQ/8/MV/eVcFNdeJ/GYhrFuUc8K7WuPhRhTSdMkCp8VMzhUq85w== + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" + integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== "@types/multer@^1.4.7": - version "1.4.8" - resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.8.tgz#8d98c36f6a4e0b228a9f262cd66e881d7cd64039" - integrity sha512-VMZOW6mnmMMhA5m3fsCdXBwFwC+a+27/8gctNMuQC4f7UtWcF79KAFGoIfKZ4iqrElgWIa3j5vhMJDp0iikQ1g== + version "1.4.11" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.11.tgz#c70792670513b4af1159a2b60bf48cc932af55c5" + integrity sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w== dependencies: "@types/express" "*" "@types/mustache@^4.1.2": - version "4.2.3" - resolved "https://registry.yarnpkg.com/@types/mustache/-/mustache-4.2.3.tgz#11ae9d7cd67c60746e3125baa8e5989db781d704" - integrity sha512-MG+oI3oelPKLN2gpkel08v6Tp6zU2zZQRq+eSpRsFtLNTd2kxZolOHQTmQQs0wqXTLOqs+ri3tRUaagH5u0quw== + version "4.2.5" + resolved "https://registry.yarnpkg.com/@types/mustache/-/mustache-4.2.5.tgz#9129f0d6857f976e00e171bbb3460e4b702f84ef" + integrity sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA== "@types/node-abi@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/node-abi/-/node-abi-3.0.1.tgz#6a8d3d6f94e9be4b9a427efac48c43c6b14af660" - integrity sha512-3fTqMJ2QY/0nKHvLIKDVO9LRx5Wres+O8J05H+cmwOw8jN/eyVm7tpCnEfrjt2jjpXlgLNEehyvqWomX5gOPqg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/node-abi/-/node-abi-3.0.3.tgz#a8334d75fe45ccd4cdb2a6c1ae82540a7a76828c" + integrity sha512-5oos6sivyXcDEuVC5oX3+wLwfgrGZu4NIOn826PGAjPCHsqp2zSPTGU7H1Tv+GZBOiDUY3nBXY1MdaofSEt4fw== -"@types/node-fetch@^2.5.7": - version "2.6.6" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.6.tgz#b72f3f4bc0c0afee1c0bc9cff68e041d01e3e779" - integrity sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw== +"@types/node-fetch@^2.5.7", "@types/node-fetch@^2.6.4": + version "2.6.11" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" + integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== dependencies: "@types/node" "*" form-data "^4.0.0" -"@types/node@*", "@types/node@18", "@types/node@>=10.0.0", "@types/node@^10.14.22", "@types/node@^16.11.26", "@types/node@^18.11.18": - version "18.18.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.8.tgz#2b285361f2357c8c8578ec86b5d097c7f464cfd6" - integrity sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ== +"@types/node@*", "@types/node@18", "@types/node@>=10.0.0", "@types/node@^10.14.22", "@types/node@^18.11.18", "@types/node@^20.9.0": + version "18.19.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.18.tgz#7526471b28828d1fef1f7e4960fb9477e6e4369c" + integrity sha512-80CP7B8y4PzZF0GWx15/gVWRrB5y/bIjNI84NK3cmQJu0WZwvmj2WMA5LcofQFVfLqqCSp545+U2LsrVzX36Zg== dependencies: undici-types "~5.26.4" "@types/normalize-package-data@^2.4.0": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz#9b0e3e8533fe5024ad32d6637eb9589988b6fdca" - integrity sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/p-queue@^2.3.1": version "2.3.2" @@ -2104,132 +2266,124 @@ integrity sha512-eKAv5Ql6k78dh3ULCsSBxX6bFNuGjTmof5Q/T6PiECDq0Yf8IIn46jCyp3RJvCi8owaEmm3DZH1PEImjBMd/vQ== "@types/prop-types@*": - version "15.7.8" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3" - integrity sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ== + version "15.7.11" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" + integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== "@types/proxy-from-env@^1.0.1": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/proxy-from-env/-/proxy-from-env-1.0.2.tgz#f0eb1c492b98d64986df19a16ee3f196b25d3306" - integrity sha512-69oo0n4YzfxciixHK4c7AgEc4QDYQ4NVIWb/LpgqKqf3bpDqqxyEDTBvGDKQvO7PGO6bnsq932qZtJosAf66ow== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/proxy-from-env/-/proxy-from-env-1.0.4.tgz#0a0545768f2d6c16b81a84ffefb53b423807907c" + integrity sha512-TPR9/bCZAr3V1eHN4G3LD3OLicdJjqX1QRXWuNcCYgE66f/K8jO2ZRtHxI2D9MbnuUP6+qiKSS8eUHp6TFHGCw== dependencies: "@types/node" "*" "@types/ps-tree@^1.1.0": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@types/ps-tree/-/ps-tree-1.1.3.tgz#12a05ebbdc253ed2b2a6055560667e60814791d0" - integrity sha512-J8IrehehphLtxpABSekURTw9jthrlLcM4llH1I2fZ0zKaxq8jI/O1+Q/tabAJgBY/ffoqDxPRNYBM1lFUXm0lw== + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/ps-tree/-/ps-tree-1.1.6.tgz#fbb22dabe3d64b79295f37ce0afb7320a26ac9a6" + integrity sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ== "@types/qs@*": - version "6.9.8" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.8.tgz#f2a7de3c107b89b441e071d5472e6b726b4adf45" - integrity sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg== + version "6.9.11" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda" + integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ== "@types/range-parser@*": - version "1.2.5" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.5.tgz#38bd1733ae299620771bd414837ade2e57757498" - integrity sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA== + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/react-dom@^18.0.6": - version "18.2.12" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.12.tgz#58479c463d1e0b7f1ee7cd80e09186189f9ec32d" - integrity sha512-QWZuiA/7J/hPIGocXreCRbx7wyoeet9ooxfbSA+zbIWqyQEE7GMtRn4A37BdYyksnN+/NDnWgfxZH9UVGDw1hg== + version "18.2.19" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.19.tgz#b84b7c30c635a6c26c6a6dfbb599b2da9788be58" + integrity sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA== dependencies: "@types/react" "*" "@types/react@*", "@types/react@^18.0.15": - version "18.2.27" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.27.tgz#746e52b06f3ccd5d7a724fd53769b70792601440" - integrity sha512-Wfv7B7FZiR2r3MIqbAlXoY1+tXm4bOqfz4oRr+nyXdBqapDBZ0l/IGcSlAfvxIHEEJjkPU0MYAc/BlFPOcrgLw== + version "18.2.59" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.59.tgz#14c7bcab22e2ce71d9eaa02f78d3d55067724d7f" + integrity sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" csstype "^3.0.2" "@types/readdir-glob@*": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.2.tgz#688a206aa258a3a5d17c7b053da3b9e04eabf431" - integrity sha512-vwAYrNN/8yhp/FJRU6HUSD0yk6xfoOS8HrZa8ZL7j+X8hJpaC1hTcAiXX2IxaAkkvrz9mLyoEhYZTE3cEYvA9Q== + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.5.tgz#21a4a98898fc606cb568ad815f2a0eedc24d412a" + integrity sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg== dependencies: "@types/node" "*" "@types/responselike@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.1.tgz#1dd57e54509b3b95c7958e52709567077019d65d" - integrity sha512-TiGnitEDxj2X0j+98Eqk5lv/Cij8oHd32bU4D/Yw6AOq7vvTk0gSD2GPj0G/HkvhMoVsdlhYF4yqqlyPBTM6Sg== - dependencies: - "@types/node" "*" - -"@types/rimraf@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.5.tgz#368fb04d59630b727fc05a74d2ca557f64a8ef98" - integrity sha512-YyP+VfeaqAyFmXoTh3HChxOQMyjByRMsHU7kc5KOJkSlXudhMhQIALbYV7rHh/l8d2lX3VUQzprrcAgWdRuU8g== + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50" + integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw== dependencies: - "@types/glob" "*" "@types/node" "*" "@types/route-parser@^0.1.1": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@types/route-parser/-/route-parser-0.1.5.tgz#5fee03ea01d2d457603eab3e46f4fa52573fb5c8" - integrity sha512-W17Tv0Y3uecmsqisMC5HwobDSEy7RXQfBxnbcBnVP0f6QbxFWCK+dEtC0u259nZFRgTYXHKaKbZzCtMgiYYAqg== + version "0.1.7" + resolved "https://registry.yarnpkg.com/@types/route-parser/-/route-parser-0.1.7.tgz#76d324537c9f0aaf65c96588c6ab5f3b84ae1505" + integrity sha512-haO+3HVio/4w+yuMJTjqfSo0ivOV8WnXaOReVD6QN729UGBEyizWNGc2Jd0OLsJDucIod4aJSsPLBeLj2uzMCQ== "@types/safer-buffer@^2.1.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@types/safer-buffer/-/safer-buffer-2.1.1.tgz#7d504d3d5b9cba87723543d0da3f47649d4feb52" - integrity sha512-L/QB8WCfXIRPguK8h3L+o1QO9b2NltRpj6y8dYusvzGPJhPZtw9lWYb9gmLvf30qS7j6cZ/wUBXXu36UEtH1XQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/@types/safer-buffer/-/safer-buffer-2.1.3.tgz#901b21c2da54344cf73a205fb5d400592a43b5fd" + integrity sha512-5o3RcpBa7mUFnnnoMa9UIGOf9naD4DCKKMYzqkm9OSY7NNTd26k7adNC+fphcVRI9BUJglBs2yJQiRZYqBF/8w== dependencies: "@types/node" "*" "@types/scheduler@*": - version "0.16.4" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.4.tgz#fedc3e5b15c26dc18faae96bf1317487cb3658cf" - integrity sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ== + version "0.16.8" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" + integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== "@types/semver@^7.5.0": - version "7.5.3" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" - integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== "@types/send@*": - version "0.17.2" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.2.tgz#af78a4495e3c2b79bfbdac3955fdd50e03cc98f2" - integrity sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw== + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== dependencies: "@types/mime" "^1" "@types/node" "*" "@types/serve-static@*": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.3.tgz#2cfacfd1fd4520bbc3e292cca432d5e8e2e3ee61" - integrity sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg== + version "1.15.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033" + integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ== dependencies: "@types/http-errors" "*" "@types/mime" "*" "@types/node" "*" "@types/sinon@^10.0.6": - version "10.0.19" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.19.tgz#752b752bc40bb5af0bb1aec29bde49b139b91d35" - integrity sha512-MWZNGPSchIdDfb5FL+VFi4zHsHbNOTQEgjqFQk7HazXSXwUU9PAX3z9XBqb3AJGYr9YwrtCtaSMsT3brYsN/jQ== + version "10.0.20" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.20.tgz#f1585debf4c0d99f9938f4111e5479fb74865146" + integrity sha512-2APKKruFNCAZgx3daAyACGzWuJ028VVCUDk6o2rw/Z4PXT0ogwdV4KUegW0MwVs0Zu59auPXbbuBJHF12Sx1Eg== dependencies: "@types/sinonjs__fake-timers" "*" "@types/sinonjs__fake-timers@*": - version "8.1.3" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.3.tgz#575789c5cf6d410cb288b0b4affaf7e6da44ca51" - integrity sha512-4g+2YyWe0Ve+LBh+WUm1697PD0Kdi6coG1eU0YjQbwx61AZ8XbEpL1zIT6WjuUKrCMCROpEaYQPDjBnDouBVAQ== + version "8.1.5" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2" + integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ== "@types/ssh2-sftp-client@^9.0.0": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@types/ssh2-sftp-client/-/ssh2-sftp-client-9.0.1.tgz#653e6088d34a1f7ffa32253d7fbcf853a318109e" - integrity sha512-jz3I1vFxUezHNOl5Bppj1AiltsVh3exudiLJI3ImOz80pSWMDb+aCT5qBHSWfQyJd5QOUEV7/+jSewIVNvwzrg== + version "9.0.3" + resolved "https://registry.yarnpkg.com/@types/ssh2-sftp-client/-/ssh2-sftp-client-9.0.3.tgz#dffe7fce6dd49fbda0823508ff0ce11c111dba13" + integrity sha512-pkCiS/NYvfc8S6xq3TvHAIPhQvBcl9Z1kMFxS8yNsqxmg/8ozzglnT4TrfpYBR1hlBky3r+fYntdZ5WnvvlKoQ== dependencies: "@types/ssh2" "*" "@types/ssh2@*", "@types/ssh2@^1.11.11": - version "1.11.14" - resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.11.14.tgz#d3bebf27fa508add5ddb65d0c945ca5329e669fb" - integrity sha512-O/U38mvV4jVVrdtZz8KpmitkmeD/PUDeDNNueQhm34166dmaqb1iZ3sfarSxBArM2/iX4PZVJY3EOta0Zks9hw== + version "1.11.19" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.11.19.tgz#4f2ec691b0674ea1590915fe5114a9aeae0eb41d" + integrity sha512-ydbQAqEcdNVy2t1w7dMh6eWMr+iOgtEkqM/3K9RMijMaok/ER7L8GW6PwsOypHCN++M+c8S/UR9SgMqNIFstbA== dependencies: "@types/node" "^18.11.18" @@ -2242,28 +2396,28 @@ "@types/tar-stream" "*" "@types/tar-stream@*": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@types/tar-stream/-/tar-stream-3.1.1.tgz#a0d936ec27c732e5287a84055b849637ee609974" - integrity sha512-/1E+a09mAFQwhlEHqiS3LuNWIBiyrn0HqUWZk2IyGzodu9zkXbaT5vl94iGlZGnG2IONVFZd84SFhns3MhhAQQ== + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/tar-stream/-/tar-stream-3.1.3.tgz#f61427229691eda1b7d5719f34acdc4fc8a558ce" + integrity sha512-Zbnx4wpkWBMBSu5CytMbrT5ZpMiF55qgM+EpHzR4yIDu7mv52cej8hTkOc6K+LzpkOAbxwn/m7j3iO+/l42YkQ== dependencies: "@types/node" "*" "@types/temp@^0.9.1": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@types/temp/-/temp-0.9.2.tgz#5f134aaf5c9fd6ca2e3f93a6f84f19c6fbd1477c" - integrity sha512-n5sIDpSsilIPZU7i9R3Ts0JZEGKNz3tGF2q7I74QedOFRkdHTNIbbIt8DA3+GyqoNsi1UO9MXENmqd2VlDYAmg== + version "0.9.4" + resolved "https://registry.yarnpkg.com/@types/temp/-/temp-0.9.4.tgz#69bd4b0e8fc4d54db06bd1b613c19292d333350b" + integrity sha512-+VfWIwrlept2VBTj7Y2wQnI/Xfscy1u8Pyj/puYwss6V1IblXn1x7S0S9eFh6KyBolgLCm+rUFzhFAbdkR691g== dependencies: "@types/node" "*" -"@types/tough-cookie@*", "@types/tough-cookie@^4.0.0": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.3.tgz#3d06b6769518450871fbc40770b7586334bdfd90" - integrity sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg== +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== "@types/trusted-types@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65" - integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ== + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== "@types/unzipper@^0.9.2": version "0.9.2" @@ -2272,20 +2426,20 @@ dependencies: "@types/node" "*" -"@types/uuid@^7.0.3": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.6.tgz#455e838428ae709f82c85c677dcf5115f2061ac1" - integrity sha512-U/wu4HTp6T2dUmKqDtOUKS9cYhawuf8txqKF3Jp1iMDG8fP5HtjSldcN0g4m+/h7XHU1to1/HDCT0qeeUiu0EA== +"@types/uuid@^9.0.8": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== "@types/vscode-notebook-renderer@^1.72.0": - version "1.72.1" - resolved "https://registry.yarnpkg.com/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.72.1.tgz#5605079fa6f0fa1dddc57ce11315eec0d1a8d869" - integrity sha512-yr8mLZfyuRBa1VcQZbZOmQaNymi3aPNGxVCE9UKdyyJCf8OCP+Lqwjm603H0LKkocvmYHUPXYbdBWjwbc+BX1Q== + version "1.72.3" + resolved "https://registry.yarnpkg.com/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.72.3.tgz#013380d8ddfb9a5dc4b831ff025721afa0fd308b" + integrity sha512-MfmEI3A2McbUV2WaijoTgLOAs9chwHN4WmqOedl3jdtlbzJBWIQ9ZFmQdzPa3lYr5j8DJhRg3KB5AIM/BBfg9Q== "@types/vscode@^1.50.0": - version "1.83.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.83.0.tgz#f787d1d94d0b258b9bb97947396b47c1d364e90f" - integrity sha512-3mUtHqLAVz9hegut9au4xehuBrzRE3UJiQMpoEHkNl6XHliihO7eATx2BMHs0odsmmrwjJrlixx/Pte6M3ygDQ== + version "1.86.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.86.0.tgz#5d5f233137b27e51d7ad1462600005741296357a" + integrity sha512-DnIXf2ftWv+9LWOB5OJeIeaLigLHF7fdXF6atfc7X5g2w/wVZBgk0amP7b+ub5xAuW1q7qP5YcFvOcit/DtyCQ== "@types/which@^1.3.2": version "1.3.2" @@ -2298,64 +2452,54 @@ integrity sha512-JdO/UpPm9RrtQBNVcZdt3M7j3mHO/kXaea9LBGx3UgWJd1f9BkIWP7jObLBG6ZtRyqp7KzLFEsaPhWcidVittA== "@types/ws@^8.5.5": - version "8.5.6" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.6.tgz#e9ad51f0ab79b9110c50916c9fcbddc36d373065" - integrity sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg== + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== dependencies: "@types/node" "*" "@types/yargs-parser@*": - version "21.0.1" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.1.tgz#07773d7160494d56aa882d7531aac7319ea67c3b" - integrity sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ== + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== "@types/yargs@^15": - version "15.0.16" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.16.tgz#258009dc52907e8f03041eb64ffdac297ba4b208" - integrity sha512-2FeD5qezW3FvLpZ0JpfuaEWepgNLl9b2gQYiz/ce0NhoB1W/D+VZu98phITXkADYerfr/jb7JcDcVhITsc9bwg== + version "15.0.19" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.19.tgz#328fb89e46109ecbdb70c295d96ff2f46dfd01b9" + integrity sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA== dependencies: "@types/yargs-parser" "*" "@types/yauzl@^2.9.1": - version "2.10.1" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.1.tgz#4e8f299f0934d60f36c74f59cb5a8483fd786691" - integrity sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw== + version "2.10.3" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" + integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin-tslint@^4.8.1": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-4.33.0.tgz#c0f2a5a8a53a915d6c24983888013b7e78e75b44" - integrity sha512-o3ujMErtZJPgiNRETRJefo1bFNrloocOa5dMU49OW/G+Rq92IbXTY6FSF5MOwrdQK1X+VBEcA8y6PhUPWGlYqA== - dependencies: - "@typescript-eslint/experimental-utils" "4.33.0" - lodash "^4.17.21" - -"@typescript-eslint/eslint-plugin@^4.8.1": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" - integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg== +"@typescript-eslint/eslint-plugin-tslint@^6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-6.21.0.tgz#6ae90759b88718c059907c2db4834cbf876bba02" + integrity sha512-DktcL2dSnR90VCVHXYKUz40QQ5DY2lSvnbkQJ+b1BtWhj/sNXdtlmQR6vB6b4RyEm/GMhvLFj6Pq1MvVVXLMAg== dependencies: - "@typescript-eslint/experimental-utils" "4.33.0" - "@typescript-eslint/scope-manager" "4.33.0" - debug "^4.3.1" - functional-red-black-tree "^1.0.1" - ignore "^5.1.8" - regexpp "^3.1.0" - semver "^7.3.5" - tsutils "^3.21.0" + "@typescript-eslint/utils" "6.21.0" -"@typescript-eslint/experimental-utils@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" - integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== +"@typescript-eslint/eslint-plugin@^6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" + integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== dependencies: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.33.0" - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/typescript-estree" "4.33.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/type-utils" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" "@typescript-eslint/experimental-utils@^2.19.2 || ^3.0.0": version "3.10.1" @@ -2368,33 +2512,44 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^4.8.1": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899" - integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA== +"@typescript-eslint/parser@^6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== dependencies: - "@typescript-eslint/scope-manager" "4.33.0" - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/typescript-estree" "4.33.0" - debug "^4.3.1" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" -"@typescript-eslint/scope-manager@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" - integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ== +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== dependencies: - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/visitor-keys" "4.33.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + +"@typescript-eslint/type-utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" + integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== + dependencies: + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + debug "^4.3.4" + ts-api-utils "^1.0.1" "@typescript-eslint/types@3.10.1": version "3.10.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== -"@typescript-eslint/types@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" - integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== "@typescript-eslint/typescript-estree@3.10.1": version "3.10.1" @@ -2410,18 +2565,32 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" - integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/visitor-keys" "4.33.0" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" "@typescript-eslint/visitor-keys@3.10.1": version "3.10.1" @@ -2430,18 +2599,13 @@ dependencies: eslint-visitor-keys "^1.1.0" -"@typescript-eslint/visitor-keys@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" - integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg== +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== dependencies: - "@typescript-eslint/types" "4.33.0" - eslint-visitor-keys "^2.0.0" - -"@ungap/promise-all-settled@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" - integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" "@virtuoso.dev/react-urx@^0.2.12": version "0.2.13" @@ -2456,14 +2620,14 @@ integrity sha512-iirJNv92A1ZWxoOHHDYW/1KPoi83939o83iUBQHIim0i3tMeSKEh+bxhJdTHQ86Mr4uXx9xGUTq69cp52ZP8Xw== "@vscode/codicons@*": - version "0.0.33" - resolved "https://registry.yarnpkg.com/@vscode/codicons/-/codicons-0.0.33.tgz#a56243ab5492801fff04e53c0aab0d18a6521751" - integrity sha512-VdgpnD75swH9hpXjd34VBgQ2w2quK63WljodlUcOoJDPKiV+rPjHrcUc2sjLCNKxhl6oKqmsZgwOWcDAY2GKKQ== + version "0.0.35" + resolved "https://registry.yarnpkg.com/@vscode/codicons/-/codicons-0.0.35.tgz#7424a647f39c6e71c86c1edf12bfc27196c8fba1" + integrity sha512-7iiKdA5wHVYSbO7/Mm0hiHD3i4h+9hKUe1O4hISAe/nHhagMwb2ZbFC8jU6d7Cw+JNT2dWXN2j+WHbkhT5/l2w== "@vscode/debugprotocol@^1.51.0": - version "1.63.0" - resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.63.0.tgz#f6d16c382765d2533e515939ac2857aa1ed7ba35" - integrity sha512-7gewwv69pA7gcJUhtJsru5YN7E1AwwnlBrF5mJY4R/NGInOUqOYOWHlqQwG+4AXn0nXWbcn26MHgaGI9Q26SqA== + version "1.64.0" + resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.64.0.tgz#f20d998b96474a8ca1aab868fcda08be38fa1f41" + integrity sha512-Zhf3KvB+J04M4HPE2yCvEILGVtPixXUQMLBvx4QcAtjhc5lnwlZbbt80LCsZO2B+2BH8RMgVXk3QQ5DEzEne2Q== "@vscode/proxy-agent@^0.13.2": version "0.13.2" @@ -2480,17 +2644,18 @@ "@vscode/windows-ca-certs" "^0.3.1" "@vscode/ripgrep@^1.14.2": - version "1.15.5" - resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.15.5.tgz#26025884bbc3a8b40dfc29f5bda4b87b47bd7356" - integrity sha512-PVvKNEmtnlek3i4MJMaB910dz46CKQqcIY2gKR3PSlfz/ZPlSYuSuyQMS7iK20KL4hGUdSbWt964B5S5EIojqw== + version "1.15.9" + resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.15.9.tgz#92279f7f28e1e49ad9a89603e10b17a4c7f9f5f1" + integrity sha512-4q2PXRvUvr3bF+LsfrifmUZgSPmCNcUZo6SbEAZgArIChchkezaxLoIeQMJe/z3CCKStvaVKpBXLxN3Z8lQjFQ== dependencies: - https-proxy-agent "^5.0.0" + https-proxy-agent "^7.0.2" proxy-from-env "^1.1.0" + yauzl "^2.9.2" "@vscode/vsce@^2.15.0": - version "2.21.1" - resolved "https://registry.yarnpkg.com/@vscode/vsce/-/vsce-2.21.1.tgz#793c78d992483b428611a3927211a9640041be14" - integrity sha512-f45/aT+HTubfCU2oC7IaWnH9NjOWp668ML002QiFObFRVUCoLtcwepp9mmql/ArFUy+HCHp54Xrq4koTcOD6TA== + version "2.24.0" + resolved "https://registry.yarnpkg.com/@vscode/vsce/-/vsce-2.24.0.tgz#7f835b9fdd5bfedcecd62a6c4d684841a74974d4" + integrity sha512-p6CIXpH5HXDqmUkgFXvIKTjZpZxy/uDx4d/UsfhS9vQUun43KDNUbYeZocyAHgqcJlPEurgArHz9te1PPiqPyA== dependencies: azure-devops-node-api "^11.0.1" chalk "^2.4.2" @@ -2708,6 +2873,13 @@ abbrev@^1.0.0: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -2716,14 +2888,6 @@ accepts@~1.3.4, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-globals@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" - integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== - dependencies: - acorn "^8.1.0" - acorn-walk "^8.0.2" - acorn-import-assertions@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" @@ -2734,26 +2898,26 @@ acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.0.2: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.1.0, acorn@^8.7.1, acorn@^8.8.2: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.7.1, acorn@^8.8.2: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" integrity sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ== +advanced-mark.js@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/advanced-mark.js/-/advanced-mark.js-2.6.0.tgz#86ea8b81152b543db91b1602df4e69282c3b0083" + integrity sha512-b6Q7iNkXk1BTUvmbvtig+/p3Z54qDQqOoOKPJUZXYtCdgeZPz/qj9xZs6GKKQA2/OIOrlNwSt/OvyIq06eYPVw== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -2761,6 +2925,20 @@ agent-base@6, agent-base@^6.0.2: dependencies: debug "4" +agent-base@^7.0.2: + version "7.1.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + +agent-base@^7.1.0, agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + agentkeepalive@^4.2.1: version "4.5.0" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" @@ -2816,23 +2994,24 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.3, ajv@^8.9.0: uri-js "^4.2.2" allure-commandline@^2.23.1: - version "2.24.1" - resolved "https://registry.yarnpkg.com/allure-commandline/-/allure-commandline-2.24.1.tgz#28eb1f28c031fa2ac0742fe85798d29202db678e" - integrity sha512-eNto3ipBq+O2B/f8/OwiS3E8R7jYENs3qv8jT7wMZmziYLANsISC9tX/FfEqR3FDiQlEOjkP7iyTEZ3ph53FPg== + version "2.27.0" + resolved "https://registry.yarnpkg.com/allure-commandline/-/allure-commandline-2.27.0.tgz#abde1a14d4b95e7f63dd727a4bb2e5df44e03fe0" + integrity sha512-KuxKEZ2Joa0LCcM9w8AWgWJgmB5d3VqSgaJhPC6pEsdRwAObT/JE8NY0u4mJ61+c2mhAPGIetGV9jgP3gzxgIg== -allure-js-commons@2.9.2: - version "2.9.2" - resolved "https://registry.yarnpkg.com/allure-js-commons/-/allure-js-commons-2.9.2.tgz#47a2e31d1e476aa565fd4c467e6e1f3540774f6a" - integrity sha512-Qvi+zMZQruklqcnqG/zHCnE209v1YiWGhO3H2aPW2aXC8Ockqd01a+w2lP4Qqp3SfC+WQDeAK2+pp+v+eNl8xQ== +allure-js-commons@2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/allure-js-commons/-/allure-js-commons-2.13.0.tgz#1b2ea72ba1f9fdc26acee49a0d556be64f458909" + integrity sha512-IUT5Lyw+pAUku89Bxp6zltY6DFLYbt6vfpjRxM4Du4wKdPDrFNSPh4iPlKo3+11uctQqO+9xWUq9jH2c6SxKKA== dependencies: properties "^1.2.1" + strip-ansi "^5.2.0" allure-playwright@^2.5.0: - version "2.9.2" - resolved "https://registry.yarnpkg.com/allure-playwright/-/allure-playwright-2.9.2.tgz#27e86f37921a456632830e9c9820188ad5844aad" - integrity sha512-N0X1c1GGLg74vdDAuq86KCekuvQ5BaqqpgcBpJj5x3y/RlQPBn84wlg8PT/ViKQM4EdbNFMXOXpa5Opufv6qCg== + version "2.13.0" + resolved "https://registry.yarnpkg.com/allure-playwright/-/allure-playwright-2.13.0.tgz#c13ccf0a49aec9fee7afdef362ddc8ae95349134" + integrity sha512-AlPZWR7Yrc71UkWtKGkmsEFv6NI02KrzjgfVVxjN/fgpUcmYpQKUFnQbzm8/hUeYrALAl6PJfS6kWFsTaBy4Qw== dependencies: - allure-js-commons "2.9.2" + allure-js-commons "2.13.0" anser@^2.0.1: version "2.1.1" @@ -2861,6 +3040,11 @@ ansi-regex@^2.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -3003,13 +3187,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-buffer-byte-length@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" - integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== dependencies: - call-bind "^1.0.2" - is-array-buffer "^3.0.1" + call-bind "^1.0.5" + is-array-buffer "^3.0.4" array-differ@^3.0.0: version "3.0.0" @@ -3026,7 +3210,7 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== -array-includes@^3.1.6: +array-includes@^3.1.6, array-includes@^3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== @@ -3054,18 +3238,29 @@ array-uniq@^1.0.1: resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== -array.prototype.findlastindex@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207" - integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA== +array.prototype.filter@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz#423771edeb417ff5914111fff4277ea0624c0d0e" + integrity sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw== dependencies: call-bind "^1.0.2" define-properties "^1.2.0" es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - get-intrinsic "^1.2.1" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + +array.prototype.findlastindex@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz#d1c50f0b3a9da191981ff8942a0aedd82794404f" + integrity sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" -array.prototype.flat@^1.3.1: +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== @@ -3075,7 +3270,7 @@ array.prototype.flat@^1.3.1: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.1: +array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== @@ -3086,27 +3281,28 @@ array.prototype.flatmap@^1.3.1: es-shim-unscopables "^1.0.0" array.prototype.tosorted@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz#620eff7442503d66c799d95503f82b475745cefd" - integrity sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg== + version "1.1.3" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" + integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - get-intrinsic "^1.2.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.1.0" + es-shim-unscopables "^1.0.2" -arraybuffer.prototype.slice@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" - integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== dependencies: - array-buffer-byte-length "^1.0.0" - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" - is-array-buffer "^3.0.2" + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" arrify@^1.0.1: @@ -3136,6 +3332,13 @@ ast-types@0.9.6: resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" integrity sha512-qEdtR2UH78yyHX/AUNfXmJTlM48XoFZKBdwi1nzkI1mJL21cmbu0cvjxjpkXJ5NENMq42H+hNs8VLJcqXLerBQ== +ast-types@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + ast-types@^0.9.2: version "0.9.14" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.14.tgz#d34ba5dffb9d15a44351fd2a9d82e4ab2838b5ba" @@ -3154,16 +3357,23 @@ async-mutex@^0.3.1: tslib "^2.3.1" async-mutex@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" - integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== + version "0.4.1" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.1.tgz#bccf55b96f2baf8df90ed798cb5544a1f6ee4c2c" + integrity sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA== dependencies: tslib "^2.4.0" +async@^2.1.4, async@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== + dependencies: + lodash "^4.17.14" + async@^3.2.3, async@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynciterator.prototype@^1.0.0: version "1.0.0" @@ -3192,32 +3402,19 @@ autosize@^4.0.2: resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.4.tgz#924f13853a466b633b9309330833936d8bccce03" integrity sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ== -available-typed-arrays@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== - -axios-cookiejar-support@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz#7b32af7d932508546c68b1fc5ba8f562884162e1" - integrity sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig== - dependencies: - is-redirect "^1.0.0" - pify "^5.0.0" - -axios@^0.21.1: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== +available-typed-arrays@^1.0.6, available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== dependencies: - follow-redirects "^1.14.0" + possible-typed-array-names "^1.0.0" -axios@^1.0.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.1.tgz#11fbaa11fc35f431193a9564109c88c1f27b585f" - integrity sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A== +axios@^1.0.0, axios@^1.6.2: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== dependencies: - follow-redirects "^1.15.0" + follow-redirects "^1.15.4" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -3229,6 +3426,11 @@ azure-devops-node-api@^11.0.1: tunnel "0.0.6" typed-rest-client "^1.8.4" +b4a@^1.6.4, b4a@^1.6.6: + version "1.6.7" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" + integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== + babel-loader@^8.2.2: version "8.3.0" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" @@ -3239,29 +3441,29 @@ babel-loader@^8.2.2: make-dir "^3.1.0" schema-utils "^2.6.5" -babel-plugin-polyfill-corejs2@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" - integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== +babel-plugin-polyfill-corejs2@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz#dbcc3c8ca758a290d47c3c6a490d59429b0d2269" + integrity sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.5.0" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.8.3: - version "0.8.4" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz#1fac2b1dcef6274e72b3c72977ed8325cb330591" - integrity sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg== +babel-plugin-polyfill-corejs3@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz#9eea32349d94556c2ad3ab9b82ebb27d4bf04a81" + integrity sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" - core-js-compat "^3.32.2" + "@babel/helper-define-polyfill-provider" "^0.5.0" + core-js-compat "^3.34.0" -babel-plugin-polyfill-regenerator@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" - integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== +babel-plugin-polyfill-regenerator@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz#8b0c8fc6434239e5d7b8a9d1f832bb2b0310f06a" + integrity sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.5.0" babel-polyfill@^6.2.0: version "6.26.0" @@ -3285,7 +3487,41 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +bare-events@^2.0.0, bare-events@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc" + integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A== + +bare-fs@^2.1.1: + version "2.3.5" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-2.3.5.tgz#05daa8e8206aeb46d13c2fe25a2cd3797b0d284a" + integrity sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw== + dependencies: + bare-events "^2.0.0" + bare-path "^2.0.0" + bare-stream "^2.0.0" + +bare-os@^2.1.0: + version "2.4.4" + resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-2.4.4.tgz#01243392eb0a6e947177bb7c8a45123d45c9b1a9" + integrity sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ== + +bare-path@^2.0.0, bare-path@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-2.1.3.tgz#594104c829ef660e43b5589ec8daef7df6cedb3e" + integrity sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA== + dependencies: + bare-os "^2.1.0" + +bare-stream@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.3.0.tgz#5bef1cab8222517315fca1385bd7f08dff57f435" + integrity sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA== + dependencies: + b4a "^1.6.6" + streamx "^2.20.0" + +base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -3295,6 +3531,18 @@ base64id@2.0.0, base64id@~2.0.0: resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== +basic-auth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +basic-ftp@^5.0.2: + version "5.0.5" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0" + integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== + bcrypt-pbkdf@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -3317,9 +3565,9 @@ bent@^7.1.0: is-stream "^2.0.0" big-integer@^1.6.17: - version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== big.js@^5.2.2: version "5.2.2" @@ -3373,21 +3621,21 @@ bluebird@~3.4.1: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" - content-type "~1.0.4" + content-type "~1.0.5" debug "2.6.9" depd "2.0.0" destroy "1.2.0" http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" + qs "6.13.0" + raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -3441,19 +3689,34 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserslist@^4.14.5, browserslist@^4.21.9, browserslist@^4.22.1: - version "4.22.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" - integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== +browserfs@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/browserfs/-/browserfs-1.4.3.tgz#92ffc6063967612daccdb8566d3fc03f521205fb" + integrity sha512-tz8HClVrzTJshcyIu8frE15cjqjcBIu15Bezxsvl/i+6f59iNCN3kznlWjz0FEb3DlnDx3gW5szxeT6D1x0s0w== + dependencies: + async "^2.1.4" + pako "^1.0.4" + +browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.22.3: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== dependencies: - caniuse-lite "^1.0.30001541" - electron-to-chromium "^1.4.535" - node-releases "^2.0.13" + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" update-browserslist-db "^1.0.13" buffer-alloc-unsafe@^1.1.0: @@ -3559,6 +3822,30 @@ bytesish@^0.4.1: resolved "https://registry.yarnpkg.com/bytesish/-/bytesish-0.4.4.tgz#f3b535a0f1153747427aee27256748cff92347e6" integrity sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ== +cacache@^16.1.0: + version "16.1.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" + integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== + dependencies: + "@npmcli/fs" "^2.1.0" + "@npmcli/move-file" "^2.0.0" + chownr "^2.0.0" + fs-minipass "^2.1.0" + glob "^8.0.1" + infer-owner "^1.0.4" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + mkdirp "^1.0.4" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^9.0.0" + tar "^6.1.11" + unique-filename "^2.0.0" + cacache@^17.0.0: version "17.1.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.4.tgz#b3ff381580b47e85c6e64f801101508e26604b35" @@ -3605,13 +3892,16 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" callsites@^3.0.0: version "3.1.0" @@ -3637,10 +3927,10 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001541: - version "1.0.30001547" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz#d4f92efc488aab3c7f92c738d3977c2a3180472b" - integrity sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA== +caniuse-lite@^1.0.30001587: + version "1.0.30001591" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz#16745e50263edc9f395895a7cd468b9f3767cf33" + integrity sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ== caseless@~0.12.0: version "0.12.0" @@ -3657,7 +3947,7 @@ chai-string@^1.4.0: resolved "https://registry.yarnpkg.com/chai-string/-/chai-string-1.5.0.tgz#0bdb2d8a5f1dbe90bc78ec493c1c1c180dd4d3d2" integrity sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw== -chai@4.3.10, chai@^4.3.10: +chai@4.3.10: version "4.3.10" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.10.tgz#d784cec635e3b7e2ffb66446a63b4e33bd390384" integrity sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g== @@ -3670,6 +3960,19 @@ chai@4.3.10, chai@^4.3.10: pathval "^1.1.1" type-detect "^4.0.8" +chai@^4.3.10: + version "4.4.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" + integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + chainsaw@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" @@ -3702,7 +4005,7 @@ chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3777,14 +4080,16 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -chromium-bidi@0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.4.4.tgz#44f25d4fa5d2f3debc3fc3948d0657194cac4407" - integrity sha512-4BX5cSaponuvVT1+SbLYTOAgDoVtX/Khoc9UsbFJ/AsPVUeFAM3RiIDFI6XFhLYMi9WmVJqh1ZH+dRpNKkKwiQ== +chromium-bidi@0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.6.4.tgz#627d76bae2819d59b61a413babe9664e0a16b71d" + integrity sha512-8zoq6ogmhQQkAKZVKO2ObFTl4uOkqoX1PlKQX3hZQ5E9cbUotcAb7h4pTNVAGGv8Z36PF3CtdOriEp/Rz82JqQ== dependencies: - mitt "3.0.0" + mitt "3.0.1" + urlpattern-polyfill "10.0.0" + zod "3.23.8" -ci-info@^3.2.0, ci-info@^3.6.1: +ci-info@^3.2.0, ci-info@^3.6.1, ci-info@^3.7.0: version "3.9.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== @@ -3807,9 +4112,9 @@ cli-spinners@2.6.1: integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== cli-spinners@^2.5.0: - version "2.9.1" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.1.tgz#9c0b9dad69a6d47cbb4333c14319b060ed395a35" - integrity sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ== + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== cli-width@^3.0.0: version "3.0.0" @@ -4057,10 +4362,10 @@ content-type@~1.0.4, content-type@~1.0.5: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -conventional-changelog-angular@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz#a9a9494c28b7165889144fd5b91573c4aa9ca541" - integrity sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg== +conventional-changelog-angular@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz#5eec8edbff15aa9b1680a8dcfbd53e2d7eb2ba7a" + integrity sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ== dependencies: compare-func "^2.0.0" @@ -4145,10 +4450,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== cookie@^0.4.0, cookie@~0.4.1: version "0.4.2" @@ -4175,12 +4480,12 @@ copy-webpack-plugin@^8.1.1: schema-utils "^3.0.0" serialize-javascript "^5.0.1" -core-js-compat@^3.31.0, core-js-compat@^3.32.2: - version "3.33.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.0.tgz#24aa230b228406450b2277b7c8bfebae932df966" - integrity sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw== +core-js-compat@^3.31.0, core-js-compat@^3.34.0: + version "3.36.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.36.0.tgz#087679119bc2fdbdefad0d45d8e5d307d45ba190" + integrity sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw== dependencies: - browserslist "^4.22.1" + browserslist "^4.22.3" core-js@^2.4.0, core-js@^2.5.0: version "2.6.12" @@ -4200,15 +4505,10 @@ cors@~2.8.5: object-assign "^4" vary "^1" -cosmiconfig@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.0.0.tgz#e9feae014eab580f858f8a0288f38997a7bebe97" - integrity sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ== - dependencies: - import-fresh "^3.2.1" - js-yaml "^4.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" +corser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" + integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ== cosmiconfig@^8.2.0: version "8.3.6" @@ -4220,7 +4520,17 @@ cosmiconfig@^8.2.0: parse-json "^5.2.0" path-type "^4.0.0" -cpu-features@~0.0.8: +cosmiconfig@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" + integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== + dependencies: + env-paths "^2.2.1" + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + +cpu-features@~0.0.9: version "0.0.9" resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.9.tgz#5226b92f0f1c63122b0a3eb84cb8335a4de499fc" integrity sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ== @@ -4241,12 +4551,12 @@ crc32-stream@^4.0.2: crc-32 "^1.2.0" readable-stream "^3.4.0" -cross-fetch@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== dependencies: - node-fetch "2.6.7" + cross-spawn "^7.0.1" cross-spawn@^4.0.0: version "4.0.2" @@ -4266,18 +4576,18 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: which "^2.0.1" css-loader@^6.2.0: - version "6.8.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88" - integrity sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g== + version "6.10.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.10.0.tgz#7c172b270ec7b833951b52c348861206b184a4b7" + integrity sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw== dependencies: icss-utils "^5.1.0" - postcss "^8.4.21" + postcss "^8.4.33" postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.3" - postcss-modules-scope "^3.0.0" + postcss-modules-local-by-default "^4.0.4" + postcss-modules-scope "^3.1.1" postcss-modules-values "^4.0.0" postcss-value-parser "^4.2.0" - semver "^7.3.8" + semver "^7.5.4" css-select@^5.1.0: version "5.1.0" @@ -4308,15 +4618,20 @@ cssstyle@^3.0.0: rrweb-cssom "^0.6.0" csstype@^3.0.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== +data-uri-to-buffer@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" + integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== + data-urls@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4" @@ -4350,20 +4665,13 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - debug@^3.0.1, debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -4371,6 +4679,13 @@ debug@^3.0.1, debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -4512,21 +4827,21 @@ defer-to-connect@^2.0.0: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== -define-data-property@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451" - integrity sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g== +define-data-property@^1.0.1, define-data-property@^1.1.2, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: - get-intrinsic "^1.2.1" + es-define-property "^1.0.0" + es-errors "^1.3.0" gopd "^1.0.1" - has-property-descriptors "^1.0.0" define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, define-properties@^1.2.1: +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -4535,6 +4850,15 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, de has-property-descriptors "^1.0.0" object-keys "^1.1.1" +degenerator@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" + integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== + dependencies: + ast-types "^0.13.4" + escodegen "^2.1.0" + esprima "^4.0.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -4575,15 +4899,20 @@ detect-libc@^2.0.0, detect-libc@^2.0.1: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== +detect-libc@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -devtools-protocol@0.0.1094867: - version "0.0.1094867" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1094867.tgz#2ab93908e9376bd85d4e0604aa2651258f13e374" - integrity sha512-pmMDBKiRVjh0uKK6CT1WqZmM3hBVSgD+N2MrgyV1uNizAZMw4tx6i/RTc+/uCsKSCmg0xXx7arCP/OFcIwTsiQ== +devtools-protocol@0.0.1312386: + version "0.0.1312386" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz#5ab824d6f1669ec6c6eb0fba047e73601d969052" + integrity sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA== diff-sequences@^29.6.3: version "29.6.3" @@ -4595,20 +4924,15 @@ diff@5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== -diff@^3.4.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diff@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" - integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== +diff@^5.0.0, diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== dir-glob@^2.0.0: version "2.2.2" @@ -4624,6 +4948,25 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +docker-modem@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-5.0.3.tgz#50c06f11285289f58112b5c4c4d89824541c41d0" + integrity sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg== + dependencies: + debug "^4.1.1" + readable-stream "^3.5.0" + split-ca "^1.0.1" + ssh2 "^1.15.0" + +dockerode@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-4.0.2.tgz#dedc8529a1db3ac46d186f5912389899bc309f7d" + integrity sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w== + dependencies: + "@balena/dockerignore" "^1.0.2" + docker-modem "^5.0.3" + tar-fs "~2.0.1" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -4700,9 +5043,9 @@ dotenv-expand@~10.0.0: integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== dotenv@~16.3.1: - version "16.3.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" - integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + version "16.3.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.2.tgz#3cb611ce5a63002dbabf7c281bc331f69d28f03f" + integrity sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ== drivelist@^9.0.2: version "9.2.4" @@ -4761,17 +5104,16 @@ ejs@^3.1.7: dependencies: jake "^10.8.5" -electron-mocha@^11.0.2: - version "11.0.2" - resolved "https://registry.yarnpkg.com/electron-mocha/-/electron-mocha-11.0.2.tgz#f8fd6c3af539f3c7a9aed4aba29cf12c3f408810" - integrity sha512-fOk+zUgSIsmL2cuIrd7IlK4eRhGVi1PYIB3QvqiBO+6f6AP8XLkYkT9eORlL2xwaS3yAAk02Y+4OTuhtqHPkEQ== +electron-mocha@^12.3.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/electron-mocha/-/electron-mocha-12.3.0.tgz#10b08a227667c44a3cdcb377069bcc7a13b6868e" + integrity sha512-PwAlZxe7+4aZ2ml2toC3dkAfrw5WsRo1P0P2uRYN7jLyaLQXD9VYMY22T9eI/JOhNUGaKy1dlYML429yk6/lFw== dependencies: ansi-colors "^4.1.1" electron-window "^0.8.0" - fs-extra "^10.0.0" - mocha "^9.1.1" - which "^2.0.2" - yargs "^16.2.0" + mocha "^10.4.0" + which "^4.0.0" + yargs "^17.7.2" electron-rebuild@^3.2.7: version "3.2.9" @@ -4801,10 +5143,10 @@ electron-store@^8.0.0: conf "^10.2.0" type-fest "^2.17.0" -electron-to-chromium@^1.4.535: - version "1.4.548" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.548.tgz#e695d769e0e801fa6d438b63f6bc9b80372000d6" - integrity sha512-R77KD6mXv37DOyKLN/eW1rGS61N6yHOfapNSX9w+y9DdPG83l9Gkuv7qkCFZ4Ta4JPhrjgQfYbv4Y3TnM1Hi2Q== +electron-to-chromium@^1.4.668: + version "1.4.682" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.682.tgz#27577b88ccccc810e09b05093345cf1830f1bd65" + integrity sha512-oCglfs8yYKs9RQjJFOHonSnhikPK3y+0SvSYc/YpYJV//6rqc0/hbwd0c7vgK4vrl6y2gJAwjkhkSGWK+z4KRA== electron-window@^0.8.0: version "0.8.1" @@ -4813,13 +5155,13 @@ electron-window@^0.8.0: dependencies: is-electron-renderer "^2.0.0" -electron@^23.2.4: - version "23.3.13" - resolved "https://registry.yarnpkg.com/electron/-/electron-23.3.13.tgz#bd2ae8eef83d1ed9504410fbe03598176c5f8817" - integrity sha512-BaXtHEb+KYKLouUXlUVDa/lj9pj4F5kiE0kwFdJV84Y2EU7euIDgPthfKtchhr5MVHmjtavRMIV/zAwEiSQ9rQ== +electron@^30.1.2: + version "30.3.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-30.3.1.tgz#fe27ca2a4739bec832b2edd6f46140ab46bf53a0" + integrity sha512-Ai/OZ7VlbFAVYMn9J5lyvtr+ZWyEbXDVd5wBLb5EVrp4352SRmMAmN5chcIe3n9mjzcgehV9n4Hwy15CJW+YbA== dependencies: "@electron/get" "^2.0.0" - "@types/node" "^16.11.26" + "@types/node" "^20.9.0" extract-zip "^2.0.1" emoji-regex@^8.0.0: @@ -4842,6 +5184,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -4857,9 +5204,9 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: once "^1.4.0" engine.io-client@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.2.tgz#8709e22c291d4297ae80318d3c8baeae71f0e002" - integrity sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg== + version "6.5.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.3.tgz#4cf6fa24845029b238f83c628916d9149c399bc5" + integrity sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" @@ -4868,14 +5215,14 @@ engine.io-client@~6.5.2: xmlhttprequest-ssl "~2.0.0" engine.io-parser@~5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" - integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== + version "5.2.2" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" + integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== engine.io@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.3.tgz#80b0692912cef3a417e1b7433301d6397bf0374b" - integrity sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw== + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc" + integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -4932,9 +5279,9 @@ envinfo@7.8.1: integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== envinfo@^7.7.3: - version "7.10.0" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13" - integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw== + version "7.11.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.11.1.tgz#2ffef77591057081b0129a8fd8cf6118da1b94e1" + integrity sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg== err-code@^2.0.2: version "2.0.3" @@ -4955,91 +5302,111 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.22.1: - version "1.22.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.2.tgz#90f7282d91d0ad577f505e423e52d4c1d93c1b8a" - integrity sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA== - dependencies: - array-buffer-byte-length "^1.0.0" - arraybuffer.prototype.slice "^1.0.2" - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-set-tostringtag "^2.0.1" +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.22.4: + version "1.22.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.4.tgz#26eb2e7538c3271141f5754d31aabfdb215f27bf" + integrity sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.6" + call-bind "^1.0.7" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.2" es-to-primitive "^1.2.1" function.prototype.name "^1.1.6" - get-intrinsic "^1.2.1" - get-symbol-description "^1.0.0" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" globalthis "^1.0.3" gopd "^1.0.1" - has "^1.0.3" - has-property-descriptors "^1.0.0" + has-property-descriptors "^1.0.2" has-proto "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.5" - is-array-buffer "^3.0.2" + hasown "^2.0.1" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" is-callable "^1.2.7" is-negative-zero "^2.0.2" is-regex "^1.1.4" is-shared-array-buffer "^1.0.2" is-string "^1.0.7" - is-typed-array "^1.1.12" + is-typed-array "^1.1.13" is-weakref "^1.0.2" - object-inspect "^1.12.3" + object-inspect "^1.13.1" object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.1" - safe-array-concat "^1.0.1" - safe-regex-test "^1.0.0" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.0" + safe-regex-test "^1.0.3" string.prototype.trim "^1.2.8" string.prototype.trimend "^1.0.7" string.prototype.trimstart "^1.0.7" - typed-array-buffer "^1.0.0" + typed-array-buffer "^1.0.1" typed-array-byte-length "^1.0.0" typed-array-byte-offset "^1.0.0" typed-array-length "^1.0.4" unbox-primitive "^1.0.2" - which-typed-array "^1.1.11" + which-typed-array "^1.1.14" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.0.0, es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-iterator-helpers@^1.0.12: - version "1.0.15" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz#bd81d275ac766431d19305923707c3efd9f1ae40" - integrity sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g== + version "1.0.17" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz#123d1315780df15b34eb181022da43e734388bb8" + integrity sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ== dependencies: asynciterator.prototype "^1.0.0" - call-bind "^1.0.2" + call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.1" - es-set-tostringtag "^2.0.1" - function-bind "^1.1.1" - get-intrinsic "^1.2.1" + es-abstract "^1.22.4" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.2" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" globalthis "^1.0.3" - has-property-descriptors "^1.0.0" + has-property-descriptors "^1.0.2" has-proto "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.5" + internal-slot "^1.0.7" iterator.prototype "^1.1.2" - safe-array-concat "^1.0.1" + safe-array-concat "^1.1.0" es-module-lexer@^1.2.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" - integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== + version "1.4.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5" + integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== -es-set-tostringtag@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" - integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== +es-set-tostringtag@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== dependencies: - get-intrinsic "^1.1.3" - has "^1.0.3" - has-tostringtag "^1.0.0" + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" -es-shim-unscopables@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== dependencies: - has "^1.0.3" + hasown "^2.0.0" es-to-primitive@^1.2.1: version "1.2.1" @@ -5061,9 +5428,9 @@ es6-promise@^4.1.1, es6-promise@^4.2.4: integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" @@ -5080,7 +5447,7 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escodegen@^2.0.0: +escodegen@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== @@ -5091,7 +5458,7 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-import-resolver-node@^0.3.7: +eslint-import-resolver-node@^0.3.9: version "0.3.9" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== @@ -5101,9 +5468,9 @@ eslint-import-resolver-node@^0.3.7: resolve "^1.22.4" eslint-module-utils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" - integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== + version "2.8.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" + integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== dependencies: debug "^3.2.7" @@ -5117,27 +5484,27 @@ eslint-plugin-deprecation@~1.2.1: tsutils "^3.0.0" eslint-plugin-import@^2.27.5: - version "2.28.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz#63b8b5b3c409bfc75ebaf8fb206b07ab435482c4" - integrity sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A== - dependencies: - array-includes "^3.1.6" - array.prototype.findlastindex "^1.2.2" - array.prototype.flat "^1.3.1" - array.prototype.flatmap "^1.3.1" + version "2.29.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.7" + eslint-import-resolver-node "^0.3.9" eslint-module-utils "^2.8.0" - has "^1.0.3" - is-core-module "^2.13.0" + hasown "^2.0.0" + is-core-module "^2.13.1" is-glob "^4.0.3" minimatch "^3.1.2" - object.fromentries "^2.0.6" - object.groupby "^1.0.0" - object.values "^1.1.6" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" semver "^6.3.1" - tsconfig-paths "^3.14.2" + tsconfig-paths "^3.15.0" eslint-plugin-no-null@latest: version "1.0.2" @@ -5186,13 +5553,6 @@ eslint-utils@^2.0.0, eslint-utils@^2.1.0: dependencies: eslint-visitor-keys "^1.1.0" -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" @@ -5203,6 +5563,11 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint@7: version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" @@ -5315,7 +5680,12 @@ event-stream@=3.3.4: stream-combiner "~0.0.4" through "~2.3.1" -eventemitter3@^4.0.4: +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -5393,46 +5763,46 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== -express-http-proxy@^1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-1.6.3.tgz#f3ef139ffd49a7962e7af0462bbcca557c913175" - integrity sha512-/l77JHcOUrDUX8V67E287VEUQT0lbm71gdGVoodnlWBziarYKgMcpqT7xvh/HM8Jv52phw8Bd8tY+a7QjOr7Yg== +express-http-proxy@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-2.1.1.tgz#90bd7eaee5166be968157b035eb6b499d2af2bf4" + integrity sha512-4aRQRqDQU7qNPV5av0/hLcyc0guB9UP71nCYrQEYml7YphTo8tmWf3nDZWdTJMMjFikyz9xKXaURor7Chygdwg== dependencies: debug "^3.0.1" es6-promise "^4.1.1" raw-body "^2.3.0" -express@^4.16.3: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== +express@^4.21.0: + version "4.21.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" + integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -5448,7 +5818,7 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -extract-zip@2.0.1, extract-zip@^2.0.1: +extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -5464,10 +5834,15 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@^3.2.5, fast-glob@^3.2.9: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -5496,9 +5871,9 @@ fastest-levenshtein@^1.0.12: integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" @@ -5509,6 +5884,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fflate@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + figures@3.2.0, figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -5576,13 +5956,20 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -5644,6 +6031,13 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + fix-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/fix-path/-/fix-path-3.0.0.tgz#c6b82fd5f5928e520b392a63565ebfef0ddf037e" @@ -5652,9 +6046,9 @@ fix-path@^3.0.0: shell-path "^2.1.0" flat-cache@^3.0.4: - version "3.1.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.1.tgz#a02a15fdec25a8f844ff7cc658f03dd99eb4609b" - integrity sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: flatted "^3.2.9" keyv "^4.5.3" @@ -5666,14 +6060,19 @@ flat@^5.0.2: integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.2.9: - version "3.2.9" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" - integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -follow-redirects@^1.14.0, follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== +follow-redirects@^1.0.0, follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== font-awesome@^4.7.0: version "4.7.0" @@ -5703,6 +6102,11 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +form-data-encoder@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" + integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -5712,6 +6116,14 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-node@^4.3.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2" + integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ== + dependencies: + node-domexception "1.0.0" + web-streams-polyfill "4.0.0-beta.3" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5746,10 +6158,10 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^11.1.0, fs-extra@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" - integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== +fs-extra@^11.1.0, fs-extra@^11.1.1, fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -5773,7 +6185,7 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.8: +fs-extra@^9.0.0, fs-extra@^9.0.8: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -5783,7 +6195,7 @@ fs-extra@^9.0.8: jsonfile "^6.0.1" universalify "^2.0.0" -fs-minipass@^2.0.0: +fs-minipass@^2.0.0, fs-minipass@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== @@ -5822,10 +6234,10 @@ fstream@^1.0.12: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: version "1.1.6" @@ -5890,20 +6302,21 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-func-name@^2.0.0, get-func-name@^2.0.2: +get-func-name@^2.0.1, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-package-type@^0.1.0: version "0.1.0" @@ -5950,13 +6363,24 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +get-uri@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.3.tgz#0d26697bc13cf91092e519aa63aa60ee5b6f385a" + integrity sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw== + dependencies: + basic-ftp "^5.0.2" + data-uri-to-buffer "^6.0.2" + debug "^4.3.4" + fs-extra "^11.2.0" git-raw-commits@^3.0.0: version "3.0.0" @@ -6034,17 +6458,16 @@ glob@7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== +glob@8.1.0, glob@^8.0.1, glob@^8.0.3, glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^5.0.1" once "^1.3.0" - path-is-absolute "^1.0.0" glob@^10.2.2: version "10.3.10" @@ -6057,6 +6480,18 @@ glob@^10.2.2: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" +glob@^10.3.7: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0, glob@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -6069,17 +6504,6 @@ glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, gl once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.1, glob@^8.0.3, glob@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - glob@^9.2.0: version "9.3.5" resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" @@ -6108,9 +6532,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: - version "13.23.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.23.0.tgz#ef31673c926a0976e1f61dab4dca57e0c0a8af02" - integrity sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA== + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -6121,7 +6545,7 @@ globalthis@^1.0.1, globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globby@11.1.0, globby@^11.0.3: +globby@11.1.0, globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -6174,10 +6598,10 @@ graceful-fs@4.2.11, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.1 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== handlebars@^4.7.7: version "4.7.8" @@ -6216,40 +6640,35 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: - get-intrinsic "^1.1.1" + es-define-property "^1.0.0" -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== +has-tostringtag@^1.0.0, has-tostringtag@^1.0.1, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: - has-symbols "^1.0.2" + has-symbols "^1.0.3" has-unicode@2.0.1, has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -has@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== - hasha@^5.0.0: version "5.2.2" resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" @@ -6258,7 +6677,14 @@ hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" -he@1.2.0: +hasown@^2.0.0, hasown@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa" + integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== + dependencies: + function-bind "^1.1.2" + +he@1.2.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -6321,7 +6747,7 @@ htmlparser2@^8.0.1: domutils "^3.0.1" entities "^4.4.0" -http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.1: +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== @@ -6355,6 +6781,42 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-server@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-14.1.1.tgz#d60fbb37d7c2fdff0f0fbff0d0ee6670bd285e2e" + integrity sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A== + dependencies: + basic-auth "^2.0.1" + chalk "^4.1.2" + corser "^2.0.1" + he "^1.2.0" + html-encoding-sniffer "^3.0.0" + http-proxy "^1.18.1" + mime "^1.6.0" + minimist "^1.2.6" + opener "^1.5.1" + portfinder "^1.0.28" + secure-compare "3.0.1" + union "~0.5.0" + url-join "^4.0.1" + http-status-codes@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-1.4.0.tgz#6e4c15d16ff3a9e2df03b89f3a55e1aae05fb477" @@ -6368,7 +6830,7 @@ http2-wrapper@^1.0.0-beta.5.2: quick-lru "^5.1.1" resolve-alpn "^1.0.0" -https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -6376,6 +6838,22 @@ https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" +https-proxy-agent@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" + integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg== + dependencies: + agent-base "^7.0.2" + debug "4" + +https-proxy-agent@^7.0.3, https-proxy-agent@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -6442,9 +6920,9 @@ ignore-walk@^5.0.1: minimatch "^5.0.1" ignore-walk@^6.0.0: - version "6.0.3" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-6.0.3.tgz#0fcdb6decaccda35e308a7b0948645dd9523b7bb" - integrity sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA== + version "6.0.4" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-6.0.4.tgz#89950be94b4f522225eb63a13c56badb639190e9" + integrity sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw== dependencies: minimatch "^9.0.0" @@ -6458,10 +6936,10 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.4, ignore@^5.1.8, ignore@^5.2.0: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== image-size@~0.5.0: version "0.5.5" @@ -6499,6 +6977,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -6551,13 +7034,13 @@ inquirer@^8.2.4: through "^2.3.6" wrap-ansi "^6.0.1" -internal-slot@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" - integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== +internal-slot@^1.0.5, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== dependencies: - get-intrinsic "^1.2.0" - has "^1.0.3" + es-errors "^1.3.0" + hasown "^2.0.0" side-channel "^1.0.4" interpret@^2.2.0: @@ -6566,28 +7049,30 @@ interpret@^2.2.0: integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== inversify@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/inversify/-/inversify-6.0.1.tgz#b20d35425d5d8c5cd156120237aad0008d969f02" - integrity sha512-B3ex30927698TJENHR++8FfEaJGqoWOgI6ZY5Ht/nLUsFCwHn6akbwtnUAPCgUepAnTpe2qHxhDNjoKLyz6rgQ== + version "6.0.2" + resolved "https://registry.yarnpkg.com/inversify/-/inversify-6.0.2.tgz#dc7fa0348213d789d35ffb719dea9685570989c7" + integrity sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA== -ip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" - integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== dependencies: call-bind "^1.0.2" - get-intrinsic "^1.2.0" - is-typed-array "^1.1.10" + get-intrinsic "^1.2.1" is-arrayish@^0.2.1: version "0.2.1" @@ -6635,12 +7120,12 @@ is-ci@3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.8.1, is-core-module@^2.9.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== +is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0, is-core-module@^2.8.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" + hasown "^2.0.0" is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" @@ -6723,9 +7208,9 @@ is-natural-number@^4.0.1: integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== is-negative-zero@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== is-number-object@^1.0.4: version "1.0.7" @@ -6776,11 +7261,6 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-redirect@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" - integrity sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw== - is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -6795,11 +7275,11 @@ is-set@^2.0.1: integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== is-shared-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" - integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" is-ssh@^1.4.0: version "1.4.0" @@ -6844,12 +7324,12 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" -is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: - version "1.1.12" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" - integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== dependencies: - which-typed-array "^1.1.11" + which-typed-array "^1.1.14" is-typedarray@^1.0.0: version "1.0.0" @@ -6891,18 +7371,13 @@ is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -is-wsl@^2.2.0: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -6918,15 +7393,25 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isexe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" + integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== + isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +isomorphic.js@^0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz#13eecf36f2dba53e85d355e11bf9d4208c6f7f88" + integrity sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw== + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" - integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== istanbul-lib-hook@^3.0.0: version "3.0.0" @@ -6976,9 +7461,9 @@ istanbul-lib-source-maps@^4.0.0: source-map "^0.6.1" istanbul-reports@^3.0.2: - version "3.1.6" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a" - integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg== + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" @@ -7003,6 +7488,15 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jake@^10.8.5: version "10.8.7" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" @@ -7062,24 +7556,26 @@ js-yaml@^3.10.0, js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + jschardet@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== -jsdom@^21.1.1: - version "21.1.2" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.2.tgz#6433f751b8718248d646af1cdf6662dc8a1ca7f9" - integrity sha512-sCpFmK2jv+1sjff4u7fzft+pUh2KSUbUrEHYHyfSIbGTIcmnjyp83qg6qLwdJ/I3LpTXx33ACxeRL7Lsyc6lGQ== +jsdom@^22.1.0: + version "22.1.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.1.0.tgz#0fca6d1a37fbeb7f4aac93d1090d782c56b611c8" + integrity sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw== dependencies: abab "^2.0.6" - acorn "^8.8.2" - acorn-globals "^7.0.0" cssstyle "^3.0.0" data-urls "^4.0.0" decimal.js "^10.4.3" domexception "^4.0.0" - escodegen "^2.0.0" form-data "^4.0.0" html-encoding-sniffer "^3.0.0" http-proxy-agent "^5.0.0" @@ -7125,9 +7621,9 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-parse-even-better-errors@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz#2cb2ee33069a78870a0c7e3da560026b89669cf7" - integrity sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA== + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz#02bb29fb5da90b5444581749c22cedd3597c6cb0" + integrity sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg== json-schema-traverse@^0.4.1: version "0.4.1" @@ -7149,6 +7645,16 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stable-stringify@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454" + integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg== + dependencies: + call-bind "^1.0.5" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -7166,7 +7672,7 @@ json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonc-parser@3.2.0, jsonc-parser@^3.0.0, jsonc-parser@^3.2.0: +jsonc-parser@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== @@ -7176,6 +7682,11 @@ jsonc-parser@^2.2.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== +jsonc-parser@^3.0.0, jsonc-parser@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" + integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -7192,6 +7703,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -7207,10 +7723,10 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: object.assign "^4.1.4" object.values "^1.1.6" -just-extend@^4.0.2: - version "4.2.1" - resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" - integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== +just-extend@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" + integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw== just-performance@4.3.0: version "4.3.0" @@ -7245,6 +7761,13 @@ kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + lazystream@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" @@ -7253,12 +7776,12 @@ lazystream@^1.0.0: readable-stream "^2.0.5" lerna@^7.1.1: - version "7.3.0" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-7.3.0.tgz#efecafbdce15694e2f6841256e073a3a2061053e" - integrity sha512-Dt8TH+J+c9+3MhTYcm5OxnNzXb87WG7GPNj3kidjYJjJY7KxIMDNU37qBTYRWA1h3wAeNKBplXVQYUPkGcYgkQ== + version "7.4.2" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-7.4.2.tgz#03497125d7b7c8d463eebfe17a701b16bde2ad09" + integrity sha512-gxavfzHfJ4JL30OvMunmlm4Anw7d7Tq6tdVHzUukLdS9nWnxCN/QB21qR+VJYp5tcyXogHKbdUEGh6qmeyzxSA== dependencies: - "@lerna/child-process" "7.3.0" - "@lerna/create" "7.3.0" + "@lerna/child-process" "7.4.2" + "@lerna/create" "7.4.2" "@npmcli/run-script" "6.0.2" "@nx/devkit" ">=16.5.1 < 17" "@octokit/plugin-enterprise-rest" "6.0.1" @@ -7268,7 +7791,7 @@ lerna@^7.1.1: clone-deep "4.0.1" cmd-shim "6.0.1" columnify "1.6.0" - conventional-changelog-angular "6.0.0" + conventional-changelog-angular "7.0.0" conventional-changelog-core "5.0.1" conventional-recommended-bump "7.0.1" cosmiconfig "^8.2.0" @@ -7362,6 +7885,20 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lib0@^0.2.52, lib0@^0.2.94: + version "0.2.94" + resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.94.tgz#fc28b4b65f816599f1e2f59d3401e231709535b3" + integrity sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ== + dependencies: + isomorphic.js "^0.2.4" + +lib0@^0.2.85, lib0@^0.2.86: + version "0.2.93" + resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.93.tgz#95487c2a97657313cb1d91fbcf9f6d64b7fcd062" + integrity sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q== + dependencies: + isomorphic.js "^0.2.4" + libnpmaccess@7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-7.0.2.tgz#7f056c8c933dd9c8ba771fa6493556b53c5aac52" @@ -7404,9 +7941,9 @@ lines-and-columns@^1.1.6: integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== lines-and-columns@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz#b2f0badedb556b747020ab8ea7f0373e22efac1b" - integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w== + version "2.0.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.4.tgz#d00318855905d2660d8c0822e3f5a4715855fc42" + integrity sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A== linkify-it@^3.0.1: version "3.0.3" @@ -7558,7 +8095,7 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== -lodash@^4.17.15, lodash@^4.17.21, lodash@^4.5.1: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.5.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7594,17 +8131,22 @@ loose-envify@^1.1.0, loose-envify@^1.4.0: js-tokens "^3.0.0 || ^4.0.0" loupe@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" - integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA== + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== dependencies: - get-func-name "^2.0.0" + get-func-name "^2.0.1" lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -7627,15 +8169,15 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: +lru-cache@^7.14.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: version "7.18.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== "lru-cache@^9.1.1 || ^10.0.0": - version "10.0.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" - integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== lunr@^2.3.9: version "2.3.9" @@ -7690,7 +8232,29 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" -make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1, make-fetch-happen@^11.0.3, make-fetch-happen@^11.1.1: +make-fetch-happen@^10.0.3: + version "10.2.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" + integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== + dependencies: + agentkeepalive "^4.2.1" + cacache "^16.1.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-fetch "^2.0.3" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^7.0.0" + ssri "^9.0.0" + +make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1, make-fetch-happen@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz#85ceb98079584a9523d4bf71d32996e7e208549f" integrity sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w== @@ -7781,10 +8345,10 @@ meow@^8.1.2: type-fest "^0.18.0" yargs-parser "^20.2.3" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -7801,7 +8365,7 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.4: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -7809,6 +8373,14 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +micromatch@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -7821,7 +8393,7 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.24, dependencies: mime-db "1.52.0" -mime@1.6.0, mime@^1.3.4, mime@^1.4.1: +mime@1.6.0, mime@^1.3.4, mime@^1.4.1, mime@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -7862,11 +8434,12 @@ min-indent@^1.0.0: integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== mini-css-extract-plugin@^2.6.1: - version "2.7.6" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz#282a3d38863fddcd2e0c220aaed5b90bc156564d" - integrity sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw== + version "2.8.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz#1aeae2a90a954b6426c9e8311eab36b450f553a0" + integrity sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg== dependencies: schema-utils "^4.0.0" + tapable "^2.2.1" minimatch@3.0.5: version "3.0.5" @@ -7875,13 +8448,6 @@ minimatch@3.0.5: dependencies: brace-expansion "^1.1.7" -minimatch@4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" - integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== - dependencies: - brace-expansion "^1.1.7" - minimatch@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" @@ -7889,6 +8455,13 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@9.0.3, minimatch@^9.0.0, minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -7910,10 +8483,10 @@ minimatch@^8.0.2: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.0, minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" @@ -7938,6 +8511,17 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" +minipass-fetch@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" + integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== + dependencies: + minipass "^3.1.6" + minipass-sized "^1.0.3" + minizlib "^2.1.2" + optionalDependencies: + encoding "^0.1.13" + minipass-fetch@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.4.tgz#4d4d9b9f34053af6c6e597a64be8e66e42bf45b7" @@ -7978,7 +8562,7 @@ minipass-sized@^1.0.3: dependencies: minipass "^3.0.0" -minipass@^3.0.0, minipass@^3.1.1: +minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: version "3.3.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== @@ -8000,6 +8584,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -8008,17 +8597,17 @@ minizlib@^2.1.1, minizlib@^2.1.2: minipass "^3.0.0" yallist "^4.0.0" -mitt@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd" - integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ== +mitt@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4: +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -8031,9 +8620,9 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== mocha@^10.1.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" - integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== + version "10.3.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.3.0.tgz#0e185c49e6dccf582035c05fa91084a4ff6e3fe9" + integrity sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg== dependencies: ansi-colors "4.1.1" browser-stdout "1.3.1" @@ -8042,13 +8631,12 @@ mocha@^10.1.0: diff "5.0.0" escape-string-regexp "4.0.0" find-up "5.0.0" - glob "7.2.0" + glob "8.1.0" he "1.2.0" js-yaml "4.1.0" log-symbols "4.1.0" minimatch "5.0.1" ms "2.1.3" - nanoid "3.3.3" serialize-javascript "6.0.0" strip-json-comments "3.1.1" supports-color "8.1.1" @@ -8057,32 +8645,28 @@ mocha@^10.1.0: yargs-parser "20.2.4" yargs-unparser "2.0.0" -mocha@^9.1.1: - version "9.2.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" - integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== +mocha@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.4.0.tgz#ed03db96ee9cfc6d20c56f8e2af07b961dbae261" + integrity sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA== dependencies: - "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" chokidar "3.5.3" - debug "4.3.3" + debug "4.3.4" diff "5.0.0" escape-string-regexp "4.0.0" find-up "5.0.0" - glob "7.2.0" - growl "1.10.5" + glob "8.1.0" he "1.2.0" js-yaml "4.1.0" log-symbols "4.1.0" - minimatch "4.2.1" + minimatch "5.0.1" ms "2.1.3" - nanoid "3.3.1" serialize-javascript "6.0.0" strip-json-comments "3.1.1" supports-color "8.1.1" - which "2.0.2" - workerpool "6.2.0" + workerpool "6.2.1" yargs "16.2.0" yargs-parser "20.2.4" yargs-unparser "2.0.0" @@ -8118,31 +8702,31 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0, ms@^2.1.1: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msgpackr-extract@^2.0.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.2.0.tgz#4bb749b58d9764cfdc0d91c7977a007b08e8f262" - integrity sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog== +msgpackr-extract@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz#e05ec1bb4453ddf020551bcd5daaf0092a2c279d" + integrity sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A== dependencies: - node-gyp-build-optional-packages "5.0.3" + node-gyp-build-optional-packages "5.0.7" optionalDependencies: - "@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.2.0" - "@msgpackr-extract/msgpackr-extract-darwin-x64" "2.2.0" - "@msgpackr-extract/msgpackr-extract-linux-arm" "2.2.0" - "@msgpackr-extract/msgpackr-extract-linux-arm64" "2.2.0" - "@msgpackr-extract/msgpackr-extract-linux-x64" "2.2.0" - "@msgpackr-extract/msgpackr-extract-win32-x64" "2.2.0" - -msgpackr@1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.6.1.tgz#4f3c94d6a5b819b838ffc736eddaf60eba436d20" - integrity sha512-Je+xBEfdjtvA4bKaOv8iRhjC8qX2oJwpYH4f7JrG4uMVJVmnmkAT4pjKdbztKprGj3iwjcxPzb5umVZ02Qq3tA== + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.2" + +msgpackr@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.10.2.tgz#a73de4767f76659e8c69cf9c80fdfce83937a44a" + integrity sha512-L60rsPynBvNE+8BWipKKZ9jHcSGbtyJYIwjRq0VrIvQ08cRjntGXJYW/tmciZ2IHWIY8WEW32Qa2xbh5+SKBZA== optionalDependencies: - msgpackr-extract "^2.0.2" + msgpackr-extract "^3.0.2" multer@1.4.4-lts.1: version "1.4.4-lts.1" @@ -8183,36 +8767,24 @@ mute-stream@~1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== -nan@^2.14.0, nan@^2.17.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" - integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== +nan@2.20.0, nan@^2.14.0, nan@^2.17.0, nan@^2.18.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.20.0.tgz#08c5ea813dd54ed16e5bd6505bf42af4f7838ca3" + integrity sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw== -nano@^9.0.5: - version "9.0.5" - resolved "https://registry.yarnpkg.com/nano/-/nano-9.0.5.tgz#2b767819f612907a3ac09b21f2929d4097407262" - integrity sha512-fEAhwAdXh4hDDnC8cYJtW6D8ivOmpvFAqT90+zEuQREpRkzA/mJPcI4EKv15JUdajaqiLTXNoKK6PaRF+/06DQ== +nano@^10.1.3: + version "10.1.3" + resolved "https://registry.yarnpkg.com/nano/-/nano-10.1.3.tgz#5cb1ad14add4c9c82d53a79159848dafa84e7a13" + integrity sha512-q/hKQJJH3FhkkuJ3ojbgDph2StlSXFBPNkpZBZlsvZDbuYfxKJ4VtunEeilthcZtuIplIk1zVX5o2RgKTUTO+Q== dependencies: - "@types/tough-cookie" "^4.0.0" - axios "^0.21.1" - axios-cookiejar-support "^1.0.1" - qs "^6.9.4" - tough-cookie "^4.0.0" - -nanoid@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + axios "^1.6.2" + node-abort-controller "^3.0.1" + qs "^6.11.0" -nanoid@3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" - integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== - -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== napi-build-utils@^1.0.1: version "1.0.2" @@ -8244,21 +8816,26 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +netmask@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + nise@^5.1.0: - version "5.1.4" - resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0" - integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg== + version "5.1.9" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.9.tgz#0cb73b5e4499d738231a473cd89bd8afbb618139" + integrity sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww== dependencies: - "@sinonjs/commons" "^2.0.0" - "@sinonjs/fake-timers" "^10.0.2" - "@sinonjs/text-encoding" "^0.7.1" - just-extend "^4.0.2" - path-to-regexp "^1.7.0" + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers" "^11.2.2" + "@sinonjs/text-encoding" "^0.7.2" + just-extend "^6.2.0" + path-to-regexp "^6.2.1" node-abi@*, node-abi@^3.0.0, node-abi@^3.3.0: - version "3.48.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.48.0.tgz#122d132ae1ac097b0d711144560b17922de026ab" - integrity sha512-uWR/uwQyVV2iN5+Wkf1/oQxOR9YjU7gBclJLg2qK7GDvVohcnY6LaBXKV89N79EQFyN4/e43O32yQYE5QdFYTA== + version "3.56.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.56.0.tgz#ca807d5ff735ac6bbbd684ae3ff2debc1c2a40a7" + integrity sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q== dependencies: semver "^7.3.5" @@ -8269,6 +8846,11 @@ node-abi@^2.21.0, node-abi@^2.7.0: dependencies: semver "^5.4.1" +node-abort-controller@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + node-addon-api@^3.0.0, node-addon-api@^3.0.2, node-addon-api@^3.1.0, node-addon-api@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" @@ -8279,10 +8861,10 @@ node-addon-api@^4.3.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== -node-addon-api@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" - integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== node-api-version@^0.1.4: version "0.1.4" @@ -8291,6 +8873,11 @@ node-api-version@^0.1.4: dependencies: semver "^7.3.5" +node-domexception@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -8305,26 +8892,26 @@ node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-gyp-build-optional-packages@5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" - integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== +node-gyp-build-optional-packages@5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3" + integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w== node-gyp-build@^4.2.1, node-gyp-build@^4.3.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" - integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ== + version "4.8.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" + integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== node-gyp@^9.0.0: - version "9.4.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.0.tgz#2a7a91c7cba4eccfd95e949369f27c9ba704f369" - integrity sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg== + version "9.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" + integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== dependencies: env-paths "^2.2.0" exponential-backoff "^3.1.1" glob "^7.1.4" graceful-fs "^4.2.6" - make-fetch-happen "^11.0.3" + make-fetch-happen "^10.0.3" nopt "^6.0.0" npmlog "^6.0.0" rimraf "^3.0.2" @@ -8351,17 +8938,17 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-pty@0.11.0-beta17: - version "0.11.0-beta17" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.11.0-beta17.tgz#7df6a60dced6bf7a3a282b65cf51980c68954af6" - integrity sha512-JALo4LgYKmzmmXI23CIfS6DpCuno647YJpNg3RT6jCKTHWrt+RHeB6JAlb/pJG9dFNSeaiIAWD+0waEg2AzlfA== +node-pty@0.11.0-beta24: + version "0.11.0-beta24" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.11.0-beta24.tgz#084841017187656edaf14b459946c4a1d7cf8392" + integrity sha512-CzItw3hitX+wnpw9dHA/A+kcbV7ETNKrsyQJ+s0ZGzsu70+CSGuIGPLPfMnAc17vOrQktxjyRQfaqij75GVJFw== dependencies: - nan "^2.14.0" + nan "^2.17.0" -node-releases@^2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" - integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== node-ssh@^12.0.1: version "12.0.5" @@ -8375,6 +8962,16 @@ node-ssh@^12.0.1: shell-escape "^0.2.0" ssh2 "^1.5.0" +nodejs-file-downloader@4.13.0: + version "4.13.0" + resolved "https://registry.yarnpkg.com/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz#da87c30081de5ff4e8b864062c98cdec03e66ad0" + integrity sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ== + dependencies: + follow-redirects "^1.15.6" + https-proxy-agent "^5.0.0" + mime-types "^2.1.27" + sanitize-filename "^1.6.3" + noop-logger@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" @@ -8569,13 +9166,6 @@ npmlog@^6.0.0, npmlog@^6.0.2: gauge "^4.0.3" set-blocking "^2.0.0" -nsfw@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/nsfw/-/nsfw-2.2.4.tgz#4ed94544a63fc843b7e3ccff6668dce13d27a33a" - integrity sha512-sTRNa7VYAiy5ARP8etIBfkIfxU0METW40UinDnv0epQMe1pzj285HdXKRKkdrV3rRzMNcuNZn2foTNszV0x+OA== - dependencies: - node-addon-api "^5.0.0" - nth-check@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" @@ -8589,9 +9179,9 @@ number-is-nan@^1.0.0: integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== nwsapi@^2.2.4: - version "2.2.7" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" - integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== + version "2.2.12" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8" + integrity sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w== nx@16.10.0, "nx@>=16.5.1 < 17": version "16.10.0" @@ -8684,23 +9274,23 @@ object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.12.3, object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" + call-bind "^1.0.5" + define-properties "^1.2.1" has-symbols "^1.0.3" object-keys "^1.1.1" @@ -8713,7 +9303,7 @@ object.entries@^1.1.6: define-properties "^1.2.0" es-abstract "^1.22.1" -object.fromentries@^2.0.6: +object.fromentries@^2.0.6, object.fromentries@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== @@ -8722,15 +9312,16 @@ object.fromentries@^2.0.6: define-properties "^1.2.0" es-abstract "^1.22.1" -object.groupby@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee" - integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ== +object.groupby@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.2.tgz#494800ff5bab78fd0eff2835ec859066e00192ec" + integrity sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + array.prototype.filter "^1.0.3" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.0.0" object.hasown@^1.1.2: version "1.1.3" @@ -8740,7 +9331,7 @@ object.hasown@^1.1.2: define-properties "^1.2.0" es-abstract "^1.22.1" -object.values@^1.1.6: +object.values@^1.1.6, object.values@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== @@ -8756,6 +9347,13 @@ octicons@^7.1.0: dependencies: object-assign "^4.1.1" +ollama@^0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.5.8.tgz#d52f20345b4b49e26734cf2e8749dd95899c2c99" + integrity sha512-frBGdfSV34i7JybLZUeyCYDx0CMyDiG4On8xOK+cNRWM04HImhoWgIMpF4p7vTkQumadbSxOteR7SZyKqNmOXg== + dependencies: + whatwg-fetch "^3.6.20" + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -8777,6 +9375,34 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open-collaboration-protocol@0.2.0, open-collaboration-protocol@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/open-collaboration-protocol/-/open-collaboration-protocol-0.2.0.tgz#f3f93f22bb5fbb46e3fd31e6bb87f52a9ce6526b" + integrity sha512-ZaLMTMyVoJJ0vPjoMXGhNZqiycbfyJPbNCkbI9uHTOYRsvZqreRAFhSd7p9RbxLJNS5xeQGNSfldrhhec94Bmg== + dependencies: + base64-js "^1.5.1" + fflate "^0.8.2" + msgpackr "^1.10.2" + semver "^7.6.2" + socket.io-client "^4.7.5" + +open-collaboration-yjs@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/open-collaboration-yjs/-/open-collaboration-yjs-0.2.0.tgz#7c7e30dba444b9f6947fe76ae02a7c3fdaec6172" + integrity sha512-HT2JU/HJObIaQMF/MHt5/5VdOnGn+bVTaTJnyYfyaa/vjqg4Z4Glas3Hc9Ua970ssP3cOIRUQoHQumM0giaxrw== + dependencies: + lib0 "^0.2.94" + open-collaboration-protocol "^0.2.0" + y-protocols "^1.0.6" + +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + open@^8.4.0: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -8786,6 +9412,24 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openai@^4.55.7: + version "4.55.7" + resolved "https://registry.yarnpkg.com/openai/-/openai-4.55.7.tgz#2bba4ae9224ad205c0d087d1412fe95421397dff" + integrity sha512-I2dpHTINt0Zk+Wlns6KzkKu77MmNW3VfIIQf5qYziEUI6t7WciG1zTobfKqdPzBmZi3TTM+3DtjPumxQdcvzwA== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + +opener@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + optionator@^0.9.1: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -8958,6 +9602,28 @@ p-waterfall@2.1.1: dependencies: p-reduce "^2.0.0" +pac-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz#0fb02496bd9fb8ae7eb11cfd98386daaac442f58" + integrity sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg== + dependencies: + "@tootallnate/quickjs-emscripten" "^0.23.0" + agent-base "^7.0.2" + debug "^4.3.4" + get-uri "^6.0.1" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.5" + pac-resolver "^7.0.1" + socks-proxy-agent "^8.0.4" + +pac-resolver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== + dependencies: + degenerator "^5.0.0" + netmask "^2.0.2" + package-hash@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506" @@ -8968,6 +9634,11 @@ package-hash@^4.0.0: lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + pacote@^15.2.0: version "15.2.0" resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.2.0.tgz#0f0dfcc3e60c7b39121b2ac612bf8596e95344d3" @@ -8992,6 +9663,11 @@ pacote@^15.2.0: ssri "^10.0.0" tar "^6.1.11" +pako@^1.0.4: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -9058,6 +9734,27 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +patch-package@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61" + integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^9.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + rimraf "^2.6.3" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.0.33" + yaml "^2.2.2" + path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" @@ -9113,17 +9810,23 @@ path-scurry@^1.10.1, path-scurry@^1.6.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: - isarray "0.0.1" + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== + +path-to-regexp@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== path-type@^3.0.0: version "3.0.0" @@ -9150,9 +9853,9 @@ pause-stream@0.0.11: through "~2.3" pdfobject@^2.0.201604172: - version "2.2.12" - resolved "https://registry.yarnpkg.com/pdfobject/-/pdfobject-2.2.12.tgz#b789e4606b69763f2f3ae501ff003f3db8231943" - integrity sha512-D0oyD/sj8j82AMaJhoyMaY1aD5TkbpU3FbJC6w9/cpJlZRpYHqAkutXw1Ca/FKjYPZmTAu58uGIfgOEaDlbY8A== + version "2.3.0" + resolved "https://registry.yarnpkg.com/pdfobject/-/pdfobject-2.3.0.tgz#467b4ffcd621518aa0e66fc191fdd669f3118b06" + integrity sha512-w/9pXDXTDs3IDmOri/w8lM/w6LHR0/F4fcBLLzH+4csSoyshQ5su0TE7k0FLHZO7aOjVLDGecqd1M89+PVpVAA== pend@~1.2.0: version "1.2.0" @@ -9174,7 +9877,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pify@5.0.0, pify@^5.0.0: +pify@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== @@ -9220,38 +9923,52 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -playwright-core@1.38.1: - version "1.38.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.38.1.tgz#75a3c470aa9576b7d7c4e274de3d79977448ba08" - integrity sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg== +playwright-core@1.41.2: + version "1.41.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.41.2.tgz#db22372c708926c697acc261f0ef8406606802d9" + integrity sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA== -playwright@1.38.1: - version "1.38.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.38.1.tgz#82ecd9bc4f4f64dbeee8a11c31793748e2528130" - integrity sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow== +playwright@1.41.2: + version "1.41.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.41.2.tgz#4e760b1c79f33d9129a8c65cc27953be6dd35042" + integrity sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A== dependencies: - playwright-core "1.38.1" + playwright-core "1.41.2" optionalDependencies: fsevents "2.3.2" +portfinder@^1.0.28: + version "1.0.32" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81" + integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg== + dependencies: + async "^2.6.4" + debug "^3.2.7" + mkdirp "^0.5.6" + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss-modules-extract-imports@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== -postcss-modules-local-by-default@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524" - integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== +postcss-modules-local-by-default@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz#7cbed92abd312b94aaea85b68226d3dec39a14e6" + integrity sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== +postcss-modules-scope@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz#32cfab55e84887c079a19bbb215e721d683ef134" + integrity sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA== dependencies: postcss-selector-parser "^6.0.4" @@ -9263,9 +9980,9 @@ postcss-modules-values@^4.0.0: icss-utils "^5.0.0" postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.0.13" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" - integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== + version "6.0.15" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535" + integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -9275,12 +9992,12 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.21: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== +postcss@^8.4.33: + version "8.4.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" + integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== dependencies: - nanoid "^3.3.6" + nanoid "^3.3.7" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -9378,7 +10095,7 @@ process-on-spawn@^1.0.0: dependencies: fromentries "^1.2.0" -progress@2.0.3, progress@^2.0.0, progress@^2.0.3: +progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -9437,7 +10154,21 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -proxy-from-env@1.1.0, proxy-from-env@^1.1.0: +proxy-agent@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.4.0.tgz#b4e2dd51dee2b377748aef8d45604c2d7608652d" + integrity sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ== + dependencies: + agent-base "^7.0.2" + debug "^4.3.4" + http-proxy-agent "^7.0.1" + https-proxy-agent "^7.0.3" + lru-cache "^7.14.1" + pac-proxy-agent "^7.0.1" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.2" + +proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -9481,26 +10212,21 @@ pump@^3.0.0: once "^1.3.1" punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -puppeteer-core@19.7.2: - version "19.7.2" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.7.2.tgz#deee9ef915829b6a1d1a3a008625c29eeb251161" - integrity sha512-PvI+fXqgP0uGJxkyZcX51bnzjFA73MODZOAv0fSD35yR7tvbqwtMV3/Y+hxQ0AMMwzxkEebP6c7po/muqxJvmQ== +puppeteer-core@23.1.0: + version "23.1.0" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-23.1.0.tgz#50703d2e27c1d73d523c25b807f6e6d95a6b1c47" + integrity sha512-SvAsu+xnLN2FMXE/59bp3s3WXp8ewqUGzVV4AQtml/2xmsciZnU/bXcCW+eETHPWQ6Agg2vTI7QzWXPpEARK2g== dependencies: - chromium-bidi "0.4.4" - cross-fetch "3.1.5" - debug "4.3.4" - devtools-protocol "0.0.1094867" - extract-zip "2.0.1" - https-proxy-agent "5.0.1" - proxy-from-env "1.1.0" - rimraf "3.0.2" - tar-fs "2.1.1" - unbzip2-stream "1.4.3" - ws "8.11.0" + "@puppeteer/browsers" "2.3.1" + chromium-bidi "0.6.4" + debug "^4.3.6" + devtools-protocol "0.0.1312386" + typed-query-selector "^2.12.0" + ws "^8.18.0" puppeteer-to-istanbul@1.4.0: version "1.4.0" @@ -9512,16 +10238,17 @@ puppeteer-to-istanbul@1.4.0: v8-to-istanbul "^1.2.1" yargs "^15.3.1" -puppeteer@19.7.2: - version "19.7.2" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-19.7.2.tgz#1b3ce99a093cc2f8f84dfb06f066d0757ea79d4b" - integrity sha512-4Lm7Qpe/LU95Svirei/jDLDvR5oMrl9BPGd7HMY5+Q28n+BhvKuW97gKkR+1LlI86bO8J3g8rG/Ll5kv9J1nlQ== +puppeteer@23.1.0: + version "23.1.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-23.1.0.tgz#3abe4980670f214c8edfe689012e83418f81f9aa" + integrity sha512-m+CyicDlGN1AVUeOsCa6/+KQydJzxfsPowL7fQy+VGNeaWafB0m8G5aGfXdfZztKMxzCsdz7KNNzbJPeG9wwFw== dependencies: - cosmiconfig "8.0.0" - https-proxy-agent "5.0.1" - progress "2.0.3" - proxy-from-env "1.1.0" - puppeteer-core "19.7.2" + "@puppeteer/browsers" "2.3.1" + chromium-bidi "0.6.4" + cosmiconfig "^9.0.0" + devtools-protocol "0.0.1312386" + puppeteer-core "23.1.0" + typed-query-selector "^2.12.0" qs@6.11.0: version "6.11.0" @@ -9530,7 +10257,14 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.9.1, qs@^6.9.4: +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +qs@^6.11.0, qs@^6.4.0, qs@^6.9.1: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== @@ -9547,6 +10281,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -9569,16 +10308,6 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - raw-body@2.5.2, raw-body@^2.3.0: version "2.5.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" @@ -9626,7 +10355,7 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-perfect-scrollbar@^1.5.3: +react-perfect-scrollbar@^1.5.3, react-perfect-scrollbar@^1.5.8: version "1.5.8" resolved "https://registry.yarnpkg.com/react-perfect-scrollbar/-/react-perfect-scrollbar-1.5.8.tgz#380959387a325c5c9d0268afc08b3f73ed5b3078" integrity sha512-bQ46m70gp/HJtiBOF3gRzBISSZn8FFGNxznTdmTG8AAwpxG1bJCyn7shrgjEvGSQ5FJEafVEiosY+ccER11OSA== @@ -9743,7 +10472,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -9792,19 +10521,20 @@ redent@^3.0.0: strip-indent "^3.0.0" reflect-metadata@^0.1.10: - version "0.1.13" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" - integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + version "0.1.14" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" + integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== reflect.getprototypeof@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz#aaccbf41aca3821b87bb71d9dcbc7ad0ba50a3f3" - integrity sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw== + version "1.0.5" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz#e0bd28b597518f16edaf9c0e292c631eb13e0674" + integrity sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.0.0" + get-intrinsic "^1.2.3" globalthis "^1.0.3" which-builtin-type "^1.1.3" @@ -9831,9 +10561,9 @@ regenerator-runtime@^0.11.0: integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== regenerator-runtime@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" - integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== regenerator-transform@^0.15.2: version "0.15.2" @@ -9842,14 +10572,15 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" - integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== +regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - set-function-name "^2.0.0" + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" regexpp@^3.1.0: version "3.2.0" @@ -9932,20 +10663,20 @@ resolve-package-path@^4.0.3: path-root "^0.1.1" resolve@^1.10.0, resolve@^1.14.2, resolve@^1.22.4, resolve@^1.3.2, resolve@^1.9.0: - version "1.22.6" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362" - integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw== + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" resolve@^2.0.0-next.4: - version "2.0.0-next.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" - integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -9974,14 +10705,14 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@2, rimraf@^2.6.1, rimraf@^2.6.2: +rimraf@2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== dependencies: glob "^7.1.3" -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -9995,6 +10726,13 @@ rimraf@^4.4.1: dependencies: glob "^9.2.0" +rimraf@^5.0.0: + version "5.0.10" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c" + integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ== + dependencies: + glob "^10.3.7" + rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -10048,33 +10786,33 @@ rxjs@^7.5.5: dependencies: tslib "^2.1.0" -safe-array-concat@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" - integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== +safe-array-concat@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.0.tgz#8d0cae9cb806d6d1c06e08ab13d847293ebe0692" + integrity sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" + call-bind "^1.0.5" + get-intrinsic "^1.2.2" has-symbols "^1.0.3" isarray "^2.0.5" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex-test@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" - integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" + call-bind "^1.0.6" + es-errors "^1.3.0" is-regex "^1.1.4" "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.2, safer-buffer@~2.1.0: @@ -10082,6 +10820,13 @@ safe-regex-test@^1.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-filename@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" + integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== + dependencies: + truncate-utf8-bytes "^1.0.0" + sax@>=0.6.0: version "1.3.0" resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" @@ -10141,6 +10886,11 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +secure-compare@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" + integrity sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw== + seek-bzip@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" @@ -10171,16 +10921,26 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +semver@^7.6.2: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +semver@^7.6.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -10218,35 +10978,48 @@ serialize-javascript@^5.0.1: randombytes "^2.1.0" serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.1.tgz#47cc5945f2c771e2cf261c6737cf9684a2a5e425" + integrity sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g== + dependencies: + define-data-property "^1.1.2" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.1" + set-function-name@^2.0.0, set-function-name@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" - integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== dependencies: - define-data-property "^1.0.1" + define-data-property "^1.1.4" + es-errors "^1.3.0" functions-have-names "^1.2.3" - has-property-descriptors "^1.0.0" + has-property-descriptors "^1.0.2" setimmediate@^1.0.5, setimmediate@~1.0.4: version "1.0.5" @@ -10313,13 +11086,24 @@ shiki@^0.10.1: vscode-textmate "5.2.0" side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + version "1.0.5" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.5.tgz#9a84546599b48909fb6af1211708d23b1946221b" + integrity sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" signal-exit@3.0.7, signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" @@ -10387,6 +11171,11 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg== +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -10402,16 +11191,27 @@ smart-buffer@^4.2.0: integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== socket.io-adapter@~2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" - integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + version "2.5.4" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz#4fdb1358667f6d68f25343353bd99bd11ee41006" + integrity sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg== dependencies: + debug "~4.3.4" ws "~8.11.0" socket.io-client@^4.5.3: - version "4.7.2" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" - integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w== + version "4.7.4" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.4.tgz#5f0e060ff34ac0a4b4c5abaaa88e0d1d928c64c8" + integrity sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +socket.io-client@^4.7.5: + version "4.7.5" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.5.tgz#919be76916989758bdc20eec63f7ee0ae45c05b7" + integrity sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.2" @@ -10427,9 +11227,9 @@ socket.io-parser@~4.2.4: debug "~4.3.1" socket.io@^4.5.3: - version "4.7.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.2.tgz#22557d76c3f3ca48f82e73d68b7add36a22df002" - integrity sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw== + version "4.7.4" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.4.tgz#2401a2d7101e4bdc64da80b140d5d8b6a8c7738b" + integrity sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw== dependencies: accepts "~1.3.4" base64id "~2.0.0" @@ -10457,12 +11257,29 @@ socks-proxy-agent@^7.0.0: debug "^4.3.3" socks "^2.6.2" +socks-proxy-agent@^8.0.2, socks-proxy-agent@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" + integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== + dependencies: + agent-base "^7.1.1" + debug "^4.3.4" + socks "^2.8.3" + socks@^2.3.3, socks@^2.6.2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + version "2.8.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.1.tgz#22c7d9dd7882649043cba0eafb49ae144e3457af" + integrity sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + +socks@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: - ip "^2.0.0" + ip-address "^9.0.5" smart-buffer "^4.2.0" sort-keys@^2.0.0: @@ -10535,9 +11352,9 @@ spdx-correct@^3.0.0: spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== spdx-expression-parse@^3.0.0: version "3.0.1" @@ -10548,9 +11365,14 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.16" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f" - integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw== + version "3.0.17" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz#887da8aa73218e51a1d917502d79863161a93f9c" + integrity sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg== + +split-ca@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" + integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== split2@^3.2.2: version "3.2.2" @@ -10573,7 +11395,7 @@ split@^1.0.1: dependencies: through "2" -sprintf-js@^1.1.2: +sprintf-js@^1.1.2, sprintf-js@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== @@ -10592,16 +11414,16 @@ ssh2-sftp-client@^9.1.0: promise-retry "^2.0.1" ssh2 "^1.12.0" -ssh2@^1.12.0, ssh2@^1.5.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.14.0.tgz#8f68440e1b768b66942c9e4e4620b2725b3555bb" - integrity sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA== +ssh2@^1.12.0, ssh2@^1.15.0, ssh2@^1.5.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.15.0.tgz#2f998455036a7f89e0df5847efb5421748d9871b" + integrity sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw== dependencies: asn1 "^0.2.6" bcrypt-pbkdf "^1.0.2" optionalDependencies: - cpu-features "~0.0.8" - nan "^2.17.0" + cpu-features "~0.0.9" + nan "^2.18.0" ssri@^10.0.0, ssri@^10.0.1: version "10.0.5" @@ -10610,7 +11432,7 @@ ssri@^10.0.0, ssri@^10.0.1: dependencies: minipass "^7.0.3" -ssri@^9.0.1: +ssri@^9.0.0, ssri@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== @@ -10639,20 +11461,23 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +streamx@^2.15.0, streamx@^2.20.0: + version "2.20.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.1.tgz#471c4f8b860f7b696feb83d5b125caab2fdbb93c" + integrity sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA== + dependencies: + fast-fifo "^1.3.2" + queue-tick "^1.0.1" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + string-argv@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738" integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA== -string-replace-loader@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-replace-loader/-/string-replace-loader-3.1.0.tgz#11ac6ee76bab80316a86af358ab773193dd57a4f" - integrity sha512-5AOMUZeX5HE/ylKDnEa/KKBqvlnFmRZudSOjVJHxhoJg9QYTwl1rECx7SLR8BBH7tfxb4Rp7EM2XVfQFxIhsbQ== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10670,6 +11495,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -10735,7 +11569,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10749,6 +11583,20 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -10880,12 +11728,22 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" -tapable@^2.1.1, tapable@^2.2.0: +tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-fs@2.1.1, tar-fs@^2.0.0: +tar-fs@^1.16.2: + version "1.16.3" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" + integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw== + dependencies: + chownr "^1.0.1" + mkdirp "^0.5.1" + pump "^1.0.0" + tar-stream "^1.1.2" + +tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -10895,15 +11753,26 @@ tar-fs@2.1.1, tar-fs@^2.0.0: pump "^3.0.0" tar-stream "^2.1.4" -tar-fs@^1.16.2: - version "1.16.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" - integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw== +tar-fs@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.6.tgz#eaccd3a67d5672f09ca8e8f9c3d2b89fa173f217" + integrity sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w== dependencies: - chownr "^1.0.1" - mkdirp "^0.5.1" - pump "^1.0.0" - tar-stream "^1.1.2" + pump "^3.0.0" + tar-stream "^3.1.5" + optionalDependencies: + bare-fs "^2.1.1" + bare-path "^2.1.0" + +tar-fs@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2" + integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" tar-stream@^1.1.2, tar-stream@^1.5.2: version "1.6.2" @@ -10918,7 +11787,7 @@ tar-stream@^1.1.2, tar-stream@^1.5.2: to-buffer "^1.1.1" xtend "^4.0.0" -tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0: +tar-stream@^2.0.0, tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -10929,6 +11798,15 @@ tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" +tar-stream@^3.1.5: + version "3.1.7" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + tar@6.1.11: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" @@ -10973,21 +11851,21 @@ temp@^0.9.1: mkdirp "^0.5.1" rimraf "~2.6.2" -terser-webpack-plugin@^5.3.7: - version "5.3.9" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" - integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== dependencies: - "@jridgewell/trace-mapping" "^0.3.17" + "@jridgewell/trace-mapping" "^0.3.20" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.1" - terser "^5.16.8" + terser "^5.26.0" -terser@^5.16.8: - version "5.21.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.21.0.tgz#d2b27e92b5e56650bc83b6defa00a110f0b124b2" - integrity sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw== +terser@^5.26.0: + version "5.28.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.28.1.tgz#bf00f7537fd3a798c352c2d67d67d65c915d1b28" + integrity sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -11003,6 +11881,13 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-decoder@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.0.tgz#85f19d4d5088e0b45cd841bdfaeac458dbffeefc" + integrity sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg== + dependencies: + b4a "^1.6.4" + text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" @@ -11062,10 +11947,10 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -tough-cookie@^4.0.0, tough-cookie@^4.1.2: - version "4.1.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" - integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== +tough-cookie@^4.1.2: + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -11120,15 +12005,27 @@ trim-repeated@^1.0.0: dependencies: escape-string-regexp "^1.0.2" +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + integrity sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ== + dependencies: + utf8-byte-length "^1.0.1" + +ts-api-utils@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + ts-md5@^1.2.2: version "1.3.1" resolved "https://registry.yarnpkg.com/ts-md5/-/ts-md5-1.3.1.tgz#f5b860c0d5241dd9bb4e909dd73991166403f511" integrity sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg== -tsconfig-paths@^3.14.2: - version "3.14.2" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" - integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.2" @@ -11149,7 +12046,12 @@ tslib@^1.10.0, tslib@^1.8.0, tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0: +tslib@^2.0.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + +tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -11180,7 +12082,7 @@ tsutils@^2.29.0: dependencies: tslib "^1.8.1" -tsutils@^3.0.0, tsutils@^3.17.1, tsutils@^3.21.0: +tsutils@^3.0.0, tsutils@^3.17.1: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== @@ -11273,44 +12175,54 @@ type-is@^1.6.4, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typed-array-buffer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" - integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== +typed-array-buffer@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" - is-typed-array "^1.1.10" + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" typed-array-byte-length@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" - integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" for-each "^0.3.3" - has-proto "^1.0.1" - is-typed-array "^1.1.10" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" typed-array-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" - integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" for-each "^0.3.3" - has-proto "^1.0.1" - is-typed-array "^1.1.10" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" typed-array-length@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" - integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + version "1.0.5" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.5.tgz#57d44da160296d8663fd63180a1802ebf25905d5" + integrity sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" for-each "^0.3.3" - is-typed-array "^1.1.9" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typed-query-selector@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2" + integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg== typed-rest-client@^1.8.4: version "1.8.11" @@ -11350,14 +12262,14 @@ typedoc@^0.22.11: shiki "^0.10.1" "typescript@>=3 < 6": - version "5.2.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" - integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== -typescript@~4.5.5: - version "4.5.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" - integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== +typescript@~5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -11388,7 +12300,7 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unbzip2-stream@1.4.3, unbzip2-stream@^1.0.9: +unbzip2-stream@^1.0.9, unbzip2-stream@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== @@ -11429,6 +12341,20 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +union@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075" + integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA== + dependencies: + qs "^6.4.0" + +unique-filename@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" + integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== + dependencies: + unique-slug "^3.0.0" + unique-filename@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" @@ -11436,6 +12362,13 @@ unique-filename@^3.0.0: dependencies: unique-slug "^4.0.0" +unique-slug@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" + integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== + dependencies: + imurmurhash "^0.1.4" + unique-slug@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-4.0.0.tgz#6bae6bb16be91351badd24cdce741f892a6532e3" @@ -11444,9 +12377,9 @@ unique-slug@^4.0.0: imurmurhash "^0.1.4" universal-user-agent@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" - integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + version "6.0.1" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.1.tgz#15f20f55da3c930c57bddbf1734c6654d5fd35aa" + integrity sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ== universalify@^0.1.0: version "0.1.2" @@ -11459,9 +12392,9 @@ universalify@^0.2.0: integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" @@ -11521,6 +12454,11 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +urlpattern-polyfill@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz#f0a03a97bfb03cdf33553e5e79a2aadd22cac8ec" + integrity sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg== + user-home@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" @@ -11528,6 +12466,11 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" +utf8-byte-length@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz#f9f63910d15536ee2b2d5dd4665389715eac5c1e" + integrity sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -11548,7 +12491,7 @@ uuid@^8.0.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: +uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== @@ -11677,6 +12620,11 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-streams-polyfill@4.0.0-beta.3: + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" + integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -11707,11 +12655,12 @@ webpack-cli@4.7.0: webpack-merge "^5.7.3" webpack-merge@^5.7.3: - version "5.9.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" - integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== dependencies: clone-deep "^4.0.1" + flat "^5.0.2" wildcard "^2.0.0" webpack-sources@^3.2.3: @@ -11720,18 +12669,18 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.76.0: - version "5.88.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" - integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== + version "5.90.3" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.90.3.tgz#37b8f74d3ded061ba789bb22b31e82eed75bd9ac" + integrity sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA== dependencies: "@types/eslint-scope" "^3.7.3" - "@types/estree" "^1.0.0" + "@types/estree" "^1.0.5" "@webassemblyjs/ast" "^1.11.5" "@webassemblyjs/wasm-edit" "^1.11.5" "@webassemblyjs/wasm-parser" "^1.11.5" acorn "^8.7.1" acorn-import-assertions "^1.9.0" - browserslist "^4.14.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" enhanced-resolve "^5.15.0" es-module-lexer "^1.2.1" @@ -11745,7 +12694,7 @@ webpack@^5.76.0: neo-async "^2.6.2" schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.3.7" + terser-webpack-plugin "^5.3.10" watchpack "^2.4.0" webpack-sources "^3.2.3" @@ -11756,6 +12705,11 @@ whatwg-encoding@^2.0.0: dependencies: iconv-lite "0.6.3" +whatwg-fetch@^3.6.20: + version "3.6.20" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== + whatwg-mimetype@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" @@ -11826,23 +12780,16 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35" integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA== -which-typed-array@^1.1.11, which-typed-array@^1.1.9: - version "1.1.11" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" - integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== +which-typed-array@^1.1.14, which-typed-array@^1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.14.tgz#1f78a111aee1e131ca66164d8bdc3ab062c95a06" + integrity sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg== dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + available-typed-arrays "^1.0.6" + call-bind "^1.0.5" for-each "^0.3.3" gopd "^1.0.1" - has-tostringtag "^1.0.0" - -which@2.0.2, which@^2.0.1, which@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" + has-tostringtag "^1.0.1" which@^1.2.0, which@^1.2.9: version "1.3.1" @@ -11851,6 +12798,13 @@ which@^1.2.0, which@^1.2.9: dependencies: isexe "^2.0.0" +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + which@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/which/-/which-3.0.1.tgz#89f1cd0c23f629a8105ffe69b8172791c87b4be1" @@ -11858,6 +12812,13 @@ which@^3.0.0: dependencies: isexe "^2.0.0" +which@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a" + integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== + dependencies: + isexe "^3.1.1" + wide-align@^1.1.0, wide-align@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" @@ -11883,17 +12844,12 @@ worker-loader@^3.0.8: loader-utils "^2.0.0" schema-utils "^3.0.0" -workerpool@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" - integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== - workerpool@6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11911,6 +12867,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -11985,16 +12950,16 @@ write-pkg@4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" -ws@8.11.0, ws@~8.11.0: +ws@^8.13.0, ws@^8.17.1, ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +ws@~8.11.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== -ws@^8.13.0, ws@^8.14.1: - version "8.14.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" - integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== - xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" @@ -12048,16 +13013,33 @@ xterm-addon-fit@^0.5.0: resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596" integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ== -xterm-addon-search@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.2.tgz#be7aa74d5ff12c901707c6ff674229f214318032" - integrity sha512-I1863mjn8P6uVrqm/X+btalVsqjAKLhnhpbP7SavAOpEkI1jJhbHU2UTp7NjeRtcKTks6UWk/ycgds5snDSejg== +xterm-addon-fit@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz#48ca99015385141918f955ca7819e85f3691d35f" + integrity sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw== + +xterm-addon-search@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0.tgz#21286f4db48aa949fbefce34bb8bc0c9d3cec627" + integrity sha512-sDUwG4CnqxUjSEFh676DlS3gsh3XYCzAvBPSvJ5OPgF3MRL3iHLPfsb06doRicLC2xXNpeG2cWk8x1qpESWJMA== xterm@^4.16.0: version "4.19.0" resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0.tgz#c0f9d09cd61de1d658f43ca75f992197add9ef6d" integrity sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ== +xterm@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0.tgz#867daf9cc826f3d45b5377320aabd996cb0fce46" + integrity sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg== + +y-protocols@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/y-protocols/-/y-protocols-1.0.6.tgz#66dad8a95752623443e8e28c0e923682d2c0d495" + integrity sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q== + dependencies: + lib0 "^0.2.85" + y18n@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" @@ -12083,6 +13065,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^2.2.2: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" + integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg== + yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" @@ -12146,7 +13133,7 @@ yargs@^15.0.2, yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^17.0.1, yargs@^17.6.2: +yargs@^17.0.1, yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -12159,7 +13146,7 @@ yargs@^17.0.1, yargs@^17.6.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yauzl@^2.10.0, yauzl@^2.3.1, yauzl@^2.4.2: +yauzl@^2.10.0, yauzl@^2.3.1, yauzl@^2.4.2, yauzl@^2.9.2: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== @@ -12174,6 +13161,13 @@ yazl@^2.2.2: dependencies: buffer-crc32 "~0.2.3" +yjs@^13.6.7: + version "13.6.15" + resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.15.tgz#5a2402632aabf83e5baf56342b4c82fe40859306" + integrity sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ== + dependencies: + lib0 "^0.2.86" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" @@ -12187,3 +13181,13 @@ zip-stream@^4.1.0: archiver-utils "^3.0.4" compress-commons "^4.1.2" readable-stream "^3.6.0" + +zod-to-json-schema@^3.23.2: + version "3.23.2" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.23.2.tgz#bc7e379c8050462538383e382964c03d8fe008f9" + integrity sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw== + +zod@3.23.8, zod@^3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==